@forzalabs/remora 2.3.0 → 2.5.0
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/CHANGELOG.md +93 -0
- package/docs/Remora - Code Sets.md +268 -0
- package/docs/Remora - Consumers.md +2224 -0
- package/docs/Remora - Producers.md +1658 -0
- package/docs/Remora - Sources.md +1150 -0
- package/documentation/README.md +262 -133
- package/documentation/default_resources/mock_data.csv +2 -2
- package/index.js +2891 -1955
- package/json_schemas/consumer-schema.json +154 -10
- package/json_schemas/producer-schema.json +0 -38
- package/package.json +4 -2
- package/workers/ExecutorWorker.js +1327 -403
|
@@ -8538,7 +8538,7 @@ var require_node2 = __commonJS({
|
|
|
8538
8538
|
var require_tail_file = __commonJS({
|
|
8539
8539
|
"../../node_modules/winston/lib/winston/tail-file.js"(exports2, module2) {
|
|
8540
8540
|
"use strict";
|
|
8541
|
-
var
|
|
8541
|
+
var fs33 = require("fs");
|
|
8542
8542
|
var { StringDecoder } = require("string_decoder");
|
|
8543
8543
|
var { Stream } = require_readable();
|
|
8544
8544
|
function noop() {
|
|
@@ -8559,7 +8559,7 @@ var require_tail_file = __commonJS({
|
|
|
8559
8559
|
stream.emit("end");
|
|
8560
8560
|
stream.emit("close");
|
|
8561
8561
|
};
|
|
8562
|
-
|
|
8562
|
+
fs33.open(options2.file, "a+", "0644", (err2, fd) => {
|
|
8563
8563
|
if (err2) {
|
|
8564
8564
|
if (!iter) {
|
|
8565
8565
|
stream.emit("error", err2);
|
|
@@ -8571,10 +8571,10 @@ var require_tail_file = __commonJS({
|
|
|
8571
8571
|
}
|
|
8572
8572
|
(function read() {
|
|
8573
8573
|
if (stream.destroyed) {
|
|
8574
|
-
|
|
8574
|
+
fs33.close(fd, noop);
|
|
8575
8575
|
return;
|
|
8576
8576
|
}
|
|
8577
|
-
return
|
|
8577
|
+
return fs33.read(fd, buffer, 0, buffer.length, pos, (error, bytes) => {
|
|
8578
8578
|
if (error) {
|
|
8579
8579
|
if (!iter) {
|
|
8580
8580
|
stream.emit("error", error);
|
|
@@ -8633,7 +8633,7 @@ var require_tail_file = __commonJS({
|
|
|
8633
8633
|
var require_file = __commonJS({
|
|
8634
8634
|
"../../node_modules/winston/lib/winston/transports/file.js"(exports2, module2) {
|
|
8635
8635
|
"use strict";
|
|
8636
|
-
var
|
|
8636
|
+
var fs33 = require("fs");
|
|
8637
8637
|
var path22 = require("path");
|
|
8638
8638
|
var asyncSeries = require_series();
|
|
8639
8639
|
var zlib3 = require("zlib");
|
|
@@ -8838,7 +8838,7 @@ var require_file = __commonJS({
|
|
|
8838
8838
|
let buff = "";
|
|
8839
8839
|
let results = [];
|
|
8840
8840
|
let row = 0;
|
|
8841
|
-
const stream =
|
|
8841
|
+
const stream = fs33.createReadStream(file, {
|
|
8842
8842
|
encoding: "utf8"
|
|
8843
8843
|
});
|
|
8844
8844
|
stream.on("error", (err2) => {
|
|
@@ -8990,7 +8990,7 @@ var require_file = __commonJS({
|
|
|
8990
8990
|
stat(callback) {
|
|
8991
8991
|
const target = this._getFile();
|
|
8992
8992
|
const fullpath = path22.join(this.dirname, target);
|
|
8993
|
-
|
|
8993
|
+
fs33.stat(fullpath, (err2, stat) => {
|
|
8994
8994
|
if (err2 && err2.code === "ENOENT") {
|
|
8995
8995
|
debug("ENOENT\xA0ok", fullpath);
|
|
8996
8996
|
this.filename = target;
|
|
@@ -9095,7 +9095,7 @@ var require_file = __commonJS({
|
|
|
9095
9095
|
_createStream(source) {
|
|
9096
9096
|
const fullpath = path22.join(this.dirname, this.filename);
|
|
9097
9097
|
debug("create stream start", fullpath, this.options);
|
|
9098
|
-
const dest =
|
|
9098
|
+
const dest = fs33.createWriteStream(fullpath, this.options).on("error", (err2) => debug(err2)).on("close", () => debug("close", dest.path, dest.bytesWritten)).on("open", () => {
|
|
9099
9099
|
debug("file open ok", fullpath);
|
|
9100
9100
|
this.emit("open", fullpath);
|
|
9101
9101
|
source.pipe(dest);
|
|
@@ -9174,7 +9174,7 @@ var require_file = __commonJS({
|
|
|
9174
9174
|
const isZipped = this.zippedArchive ? ".gz" : "";
|
|
9175
9175
|
const filePath = `${basename}${isOldest}${ext}${isZipped}`;
|
|
9176
9176
|
const target = path22.join(this.dirname, filePath);
|
|
9177
|
-
|
|
9177
|
+
fs33.unlink(target, callback);
|
|
9178
9178
|
}
|
|
9179
9179
|
/**
|
|
9180
9180
|
* Roll files forward based on integer, up to maxFiles. e.g. if base if
|
|
@@ -9197,17 +9197,17 @@ var require_file = __commonJS({
|
|
|
9197
9197
|
tasks.push(function(i6, cb) {
|
|
9198
9198
|
let fileName = `${basename}${i6 - 1}${ext}${isZipped}`;
|
|
9199
9199
|
const tmppath = path22.join(this.dirname, fileName);
|
|
9200
|
-
|
|
9200
|
+
fs33.exists(tmppath, (exists) => {
|
|
9201
9201
|
if (!exists) {
|
|
9202
9202
|
return cb(null);
|
|
9203
9203
|
}
|
|
9204
9204
|
fileName = `${basename}${i6}${ext}${isZipped}`;
|
|
9205
|
-
|
|
9205
|
+
fs33.rename(tmppath, path22.join(this.dirname, fileName), cb);
|
|
9206
9206
|
});
|
|
9207
9207
|
}.bind(this, x2));
|
|
9208
9208
|
}
|
|
9209
9209
|
asyncSeries(tasks, () => {
|
|
9210
|
-
|
|
9210
|
+
fs33.rename(
|
|
9211
9211
|
path22.join(this.dirname, `${basename}${ext}${isZipped}`),
|
|
9212
9212
|
path22.join(this.dirname, `${basename}1${ext}${isZipped}`),
|
|
9213
9213
|
callback
|
|
@@ -9223,22 +9223,22 @@ var require_file = __commonJS({
|
|
|
9223
9223
|
* @private
|
|
9224
9224
|
*/
|
|
9225
9225
|
_compressFile(src, dest, callback) {
|
|
9226
|
-
|
|
9226
|
+
fs33.access(src, fs33.F_OK, (err2) => {
|
|
9227
9227
|
if (err2) {
|
|
9228
9228
|
return callback();
|
|
9229
9229
|
}
|
|
9230
9230
|
var gzip = zlib3.createGzip();
|
|
9231
|
-
var inp =
|
|
9232
|
-
var out =
|
|
9231
|
+
var inp = fs33.createReadStream(src);
|
|
9232
|
+
var out = fs33.createWriteStream(dest);
|
|
9233
9233
|
out.on("finish", () => {
|
|
9234
|
-
|
|
9234
|
+
fs33.unlink(src, callback);
|
|
9235
9235
|
});
|
|
9236
9236
|
inp.pipe(gzip).pipe(out);
|
|
9237
9237
|
});
|
|
9238
9238
|
}
|
|
9239
9239
|
_createLogDirIfNotExist(dirPath) {
|
|
9240
|
-
if (!
|
|
9241
|
-
|
|
9240
|
+
if (!fs33.existsSync(dirPath)) {
|
|
9241
|
+
fs33.mkdirSync(dirPath, { recursive: true });
|
|
9242
9242
|
}
|
|
9243
9243
|
}
|
|
9244
9244
|
};
|
|
@@ -11809,7 +11809,7 @@ var CONSTANTS, Constants_default;
|
|
|
11809
11809
|
var init_Constants = __esm({
|
|
11810
11810
|
"../../packages/constants/src/Constants.ts"() {
|
|
11811
11811
|
CONSTANTS = {
|
|
11812
|
-
cliVersion: "2.
|
|
11812
|
+
cliVersion: "2.5.0",
|
|
11813
11813
|
backendVersion: 1,
|
|
11814
11814
|
backendPort: 5088,
|
|
11815
11815
|
workerVersion: 2,
|
|
@@ -11934,11 +11934,36 @@ var init_Constants = __esm({
|
|
|
11934
11934
|
* mode when its next line arrives — its byte counter lives in the bucket, not the stream.
|
|
11935
11935
|
*/
|
|
11936
11936
|
MAX_OPEN_SPLIT_STREAMS: 64,
|
|
11937
|
+
/**
|
|
11938
|
+
* Record layouts one FIXED output may write into a single file, which is also how many write
|
|
11939
|
+
* streams its block-ordering pass holds open. A copybook of this size is already unusual —
|
|
11940
|
+
* the NASCO POD feed writes five transaction types — and the bound stops a generated config
|
|
11941
|
+
* from opening a descriptor per distinct value of the wrong field.
|
|
11942
|
+
*/
|
|
11943
|
+
MAX_FIXED_LAYOUTS: 64,
|
|
11937
11944
|
/**
|
|
11938
11945
|
* Rows a sample may ask for out of the pipeline. A sample is a look at the data, never an
|
|
11939
11946
|
* export of it — `remora sample -s` and the preview's Data tab are both held to this.
|
|
11940
11947
|
*/
|
|
11941
|
-
MAX_SAMPLE_ROWS: 100
|
|
11948
|
+
MAX_SAMPLE_ROWS: 100,
|
|
11949
|
+
/**
|
|
11950
|
+
* Values held in memory while one parquet row group is encoded. A parquet writer buffers a
|
|
11951
|
+
* whole row group before it can compress and flush it, so this — not the dataset size — is
|
|
11952
|
+
* what bounds the export's peak memory. Budgeted in CELLS rather than rows because the two
|
|
11953
|
+
* differ by three orders of magnitude across consumers: 100,000 rows of a 4-column consumer
|
|
11954
|
+
* is 400k values, of a 1,300-field one it is 130 million.
|
|
11955
|
+
*/
|
|
11956
|
+
PARQUET_ROW_GROUP_CELLS: 1e6,
|
|
11957
|
+
/**
|
|
11958
|
+
* Floor on the derived row count, so a consumer with hundreds of columns still writes row
|
|
11959
|
+
* groups big enough to compress and to read back without a seek per handful of rows.
|
|
11960
|
+
*/
|
|
11961
|
+
PARQUET_MIN_ROW_GROUP_ROWS: 1e3,
|
|
11962
|
+
/**
|
|
11963
|
+
* Ceiling on the derived row count, so a one-column consumer does not buffer a million rows
|
|
11964
|
+
* to save a few row-group footers.
|
|
11965
|
+
*/
|
|
11966
|
+
PARQUET_MAX_ROW_GROUP_ROWS: 1e5
|
|
11942
11967
|
}
|
|
11943
11968
|
};
|
|
11944
11969
|
Constants_default = CONSTANTS;
|
|
@@ -12032,11 +12057,26 @@ var init_ExecutorScope = __esm({
|
|
|
12032
12057
|
import_path2 = __toESM(require("path"), 1);
|
|
12033
12058
|
import_fs2 = __toESM(require("fs"), 1);
|
|
12034
12059
|
import_promises = __toESM(require("fs/promises"), 1);
|
|
12060
|
+
init_src();
|
|
12035
12061
|
init_src3();
|
|
12036
12062
|
ExecutorScopeClass = class {
|
|
12037
12063
|
constructor() {
|
|
12038
12064
|
this.WORKERS_FOLDER = "workers";
|
|
12039
12065
|
this.PRODUCERS_FOLDER = "producers";
|
|
12066
|
+
/**
|
|
12067
|
+
* The scope folders THIS process opened and has not cleared. The only list `deepClear` may
|
|
12068
|
+
* delete from: two remora processes share one `.temp`, so a listing cannot tell a crashed run's
|
|
12069
|
+
* leftovers from another process's live run.
|
|
12070
|
+
*/
|
|
12071
|
+
this._ownedScopes = /* @__PURE__ */ new Set();
|
|
12072
|
+
/** Registers a scope as this process's, so a crash on the way out clears it. */
|
|
12073
|
+
this.open = (scope) => {
|
|
12074
|
+
Affirm_default(scope?.folder, "Invalid executor scope");
|
|
12075
|
+
this._ownedScopes.add(scope.folder);
|
|
12076
|
+
return scope;
|
|
12077
|
+
};
|
|
12078
|
+
/** The scopes this process still holds. Empty once every run it started has cleaned up. */
|
|
12079
|
+
this.getOwnedScopes = () => [...this._ownedScopes];
|
|
12040
12080
|
this.getBasePath = () => import_path2.default.join(Constants_default.defaults.REMORA_PATH, Constants_default.defaults.PRODUCER_TEMP_FOLDER);
|
|
12041
12081
|
this.getWorkerPath = (scope, workerId) => {
|
|
12042
12082
|
return import_path2.default.join(
|
|
@@ -12047,6 +12087,13 @@ var init_ExecutorScope = __esm({
|
|
|
12047
12087
|
`${workerId}.dataset`
|
|
12048
12088
|
);
|
|
12049
12089
|
};
|
|
12090
|
+
/**
|
|
12091
|
+
* Where a producer's source is staged before any worker reads it: a CSV extract WITH a header,
|
|
12092
|
+
* written by whichever driver read the source.
|
|
12093
|
+
*
|
|
12094
|
+
* `.extract`, not `.dataset`: the two are different formats, and one extension meaning both is
|
|
12095
|
+
* how a reader ends up applying the internal positional rules to a file that follows none.
|
|
12096
|
+
*/
|
|
12050
12097
|
this.getProducerPath = (scope, producer, sourceFileKey) => {
|
|
12051
12098
|
return import_path2.default.join(
|
|
12052
12099
|
this.getBasePath(),
|
|
@@ -12054,7 +12101,7 @@ var init_ExecutorScope = __esm({
|
|
|
12054
12101
|
scope.folder,
|
|
12055
12102
|
this.PRODUCERS_FOLDER,
|
|
12056
12103
|
producer.name,
|
|
12057
|
-
`${sourceFileKey}.
|
|
12104
|
+
`${sourceFileKey}.extract`
|
|
12058
12105
|
);
|
|
12059
12106
|
};
|
|
12060
12107
|
this.getMainPath = (scope) => {
|
|
@@ -12090,16 +12137,32 @@ var init_ExecutorScope = __esm({
|
|
|
12090
12137
|
if (import_fs2.default.existsSync(scopePath)) {
|
|
12091
12138
|
await import_promises.default.rm(scopePath, { recursive: true, force: true });
|
|
12092
12139
|
}
|
|
12140
|
+
this._ownedScopes.delete(scope.folder);
|
|
12093
12141
|
};
|
|
12142
|
+
/**
|
|
12143
|
+
* Clears what this process left open, on the way out. Synchronous: its one caller is the `exit`
|
|
12144
|
+
* handler. A run that finished cleared its own scope, so a clean exit finds nothing to do.
|
|
12145
|
+
*/
|
|
12094
12146
|
this.deepClear = () => {
|
|
12095
12147
|
const basePath = this.getBasePath();
|
|
12096
|
-
const
|
|
12097
|
-
for (const scopeFolder of openScopes) {
|
|
12148
|
+
for (const scopeFolder of this._ownedScopes) {
|
|
12098
12149
|
const scopePath = import_path2.default.join(basePath, scopeFolder);
|
|
12099
12150
|
if (import_fs2.default.existsSync(scopePath)) {
|
|
12100
12151
|
import_fs2.default.rmSync(scopePath, { recursive: true, force: true });
|
|
12101
12152
|
}
|
|
12102
12153
|
}
|
|
12154
|
+
this._ownedScopes.clear();
|
|
12155
|
+
};
|
|
12156
|
+
/**
|
|
12157
|
+
* Every scope folder on disk, whoever opened it. Answers "what is in .temp?", not "what is mine
|
|
12158
|
+
* to delete" — nothing may be removed on the strength of it (see `getOwnedScopes`). The parallel
|
|
12159
|
+
* canary samples it to watch concurrent runs come and go.
|
|
12160
|
+
*/
|
|
12161
|
+
this.getOpenScopes = () => {
|
|
12162
|
+
const basePath = this.getBasePath();
|
|
12163
|
+
if (!import_fs2.default.existsSync(basePath))
|
|
12164
|
+
return [];
|
|
12165
|
+
return import_fs2.default.readdirSync(basePath, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name).filter((folder) => folder !== "logs" && folder !== "usage");
|
|
12103
12166
|
};
|
|
12104
12167
|
this.ensurePath = (fileUri) => {
|
|
12105
12168
|
const dir = import_path2.default.dirname(fileUri);
|
|
@@ -12108,12 +12171,6 @@ var init_ExecutorScope = __esm({
|
|
|
12108
12171
|
if (!import_fs2.default.existsSync(fileUri))
|
|
12109
12172
|
import_fs2.default.writeFileSync(fileUri, "");
|
|
12110
12173
|
};
|
|
12111
|
-
this.getOpenScopes = () => {
|
|
12112
|
-
const basePath = this.getBasePath();
|
|
12113
|
-
if (!import_fs2.default.existsSync(basePath))
|
|
12114
|
-
return [];
|
|
12115
|
-
return import_fs2.default.readdirSync(basePath, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name).filter((folder) => folder !== "logs" && folder !== "usage");
|
|
12116
|
-
};
|
|
12117
12174
|
}
|
|
12118
12175
|
};
|
|
12119
12176
|
ExecutorScope = new ExecutorScopeClass();
|
|
@@ -12167,8 +12224,7 @@ var init_ProcessShutdownManager = __esm({
|
|
|
12167
12224
|
if (this._cleaned)
|
|
12168
12225
|
return;
|
|
12169
12226
|
this._cleaned = true;
|
|
12170
|
-
const
|
|
12171
|
-
const scopeCount = openScopes.length;
|
|
12227
|
+
const scopeCount = ExecutorScope_default.getOwnedScopes().length;
|
|
12172
12228
|
const shutdownState = this._shutdownState;
|
|
12173
12229
|
const shutdownDescription = `${shutdownState?.type ?? "intentional"} shutdown (${shutdownState?.reason ?? `exit:${code}`})`;
|
|
12174
12230
|
if (scopeCount === 0) {
|
|
@@ -12579,7 +12635,7 @@ var init_ResourcesUtils = __esm({
|
|
|
12579
12635
|
Affirm_default(subConsumer, `No producer found with name "${cProd.name}"`);
|
|
12580
12636
|
return this.getConsumerOutputColumns(subConsumer);
|
|
12581
12637
|
} else {
|
|
12582
|
-
|
|
12638
|
+
return producer.dimensions.map((x2) => ({
|
|
12583
12639
|
consumerAlias: null,
|
|
12584
12640
|
consumerKey: null,
|
|
12585
12641
|
nameInProducer: x2.name,
|
|
@@ -12587,15 +12643,6 @@ var init_ResourcesUtils = __esm({
|
|
|
12587
12643
|
dimension: x2,
|
|
12588
12644
|
owner: cProd.name
|
|
12589
12645
|
}));
|
|
12590
|
-
const meas = producer.measures?.map((x2) => ({
|
|
12591
|
-
consumerAlias: null,
|
|
12592
|
-
consumerKey: null,
|
|
12593
|
-
nameInProducer: x2.name,
|
|
12594
|
-
aliasInProducer: x2.name,
|
|
12595
|
-
measure: x2,
|
|
12596
|
-
owner: cProd.name
|
|
12597
|
-
})) ?? [];
|
|
12598
|
-
return [...dims, ...meas];
|
|
12599
12646
|
}
|
|
12600
12647
|
});
|
|
12601
12648
|
};
|
|
@@ -12655,7 +12702,6 @@ var init_ResourcesUtils = __esm({
|
|
|
12655
12702
|
nameInProducer: col.nameInProducer,
|
|
12656
12703
|
aliasInProducer: col.nameInProducer,
|
|
12657
12704
|
dimension: col.dimension,
|
|
12658
|
-
measure: col.measure,
|
|
12659
12705
|
owner: consumer.name
|
|
12660
12706
|
});
|
|
12661
12707
|
}
|
|
@@ -12669,7 +12715,6 @@ var init_ResourcesUtils = __esm({
|
|
|
12669
12715
|
nameInProducer: match2.name,
|
|
12670
12716
|
aliasInProducer: match2.name,
|
|
12671
12717
|
dimension: match2.column.dimension,
|
|
12672
|
-
measure: match2.column.measure,
|
|
12673
12718
|
owner: consumer.name
|
|
12674
12719
|
});
|
|
12675
12720
|
}
|
|
@@ -12683,7 +12728,6 @@ var init_ResourcesUtils = __esm({
|
|
|
12683
12728
|
nameInProducer: finalKey,
|
|
12684
12729
|
aliasInProducer: finalKey,
|
|
12685
12730
|
dimension: match?.dimension ?? { name: finalKey, type: "string" },
|
|
12686
|
-
measure: match?.measure,
|
|
12687
12731
|
owner: consumer.name
|
|
12688
12732
|
});
|
|
12689
12733
|
}
|
|
@@ -13135,14 +13179,11 @@ var init_Validator = __esm({
|
|
|
13135
13179
|
return errors2;
|
|
13136
13180
|
};
|
|
13137
13181
|
errors.push(...validateTransformations(consumer.fields));
|
|
13138
|
-
|
|
13139
|
-
|
|
13140
|
-
const duplicatesTypes = Algo_default.uniq(duplicatesOutputs.map((x2) => x2.format));
|
|
13141
|
-
errors.push(`There are outputs with the same type. (duplicates type: ${duplicatesTypes.join(" and ")})`);
|
|
13142
|
-
}
|
|
13182
|
+
if ((consumer.outputs ?? []).length > 1)
|
|
13183
|
+
errors.push(`Consumer "${consumer.name}" declares ${consumer.outputs.length} outputs (${consumer.outputs.map((x2) => x2.format).join(", ")}). A consumer writes exactly one output \u2014 only the first was ever written \u2014 so split it into one consumer per output.`);
|
|
13143
13184
|
for (const output of consumer.outputs) {
|
|
13144
13185
|
const format4 = output.format.toUpperCase();
|
|
13145
|
-
if (format4 === "CSV" || format4 === "JSON" || format4 === "PARQUET" || format4 === "TXT") {
|
|
13186
|
+
if (format4 === "CSV" || format4 === "FIXED" || format4 === "JSON" || format4 === "PARQUET" || format4 === "TXT") {
|
|
13146
13187
|
if (!output.exportDestination)
|
|
13147
13188
|
errors.push(`A static file output must have an export destination set (${format4})`);
|
|
13148
13189
|
else if (!Environment_default.getSource(output.exportDestination))
|
|
@@ -13152,6 +13193,7 @@ var init_Validator = __esm({
|
|
|
13152
13193
|
errors.push(...this.validateOmitNull(consumer));
|
|
13153
13194
|
errors.push(...this.validateHeader(consumer));
|
|
13154
13195
|
errors.push(...this.validateSplit(consumer));
|
|
13196
|
+
errors.push(...this.validateFixedOutput(consumer));
|
|
13155
13197
|
const dimensionFields = consumer.fields.filter((x2) => x2.key !== "*" && !x2.fixed && !x2.copyFrom && !x2.lineCount);
|
|
13156
13198
|
const dimensionKeys = dimensionFields.map((x2) => `${x2.from ?? "_default_"}::${x2.key}`);
|
|
13157
13199
|
const duplicateDimensionKeys = dimensionKeys.filter((k7, i6) => dimensionKeys.indexOf(k7) !== i6);
|
|
@@ -13220,7 +13262,7 @@ var init_Validator = __esm({
|
|
|
13220
13262
|
this.validateSplit = (consumer) => {
|
|
13221
13263
|
const errors = [];
|
|
13222
13264
|
const { split: split2 } = consumer;
|
|
13223
|
-
const positionalFormats = ["CSV", "TXT", "PARQUET"];
|
|
13265
|
+
const positionalFormats = ["CSV", "FIXED", "TXT", "PARQUET"];
|
|
13224
13266
|
if (!Algo_default.hasVal(split2)) return errors;
|
|
13225
13267
|
if (Algo_default.hasVal(consumer.maximumFileSize))
|
|
13226
13268
|
errors.push(`Consumer "${consumer.name}" sets both "maximumFileSize" and "split". They are the same cap: move the value to "split": { "maxFileSizeGb": ${consumer.maximumFileSize} } and remove "maximumFileSize".`);
|
|
@@ -13237,50 +13279,83 @@ var init_Validator = __esm({
|
|
|
13237
13279
|
const missingKeys = fieldKeys.includes("*") ? [] : keys.filter((key) => !fieldKeys.includes(key));
|
|
13238
13280
|
if (missingKeys.length > 0)
|
|
13239
13281
|
errors.push(`"split.by" references key(s) "${missingKeys.join(", ")}" that are not present in the consumer "${consumer.name}".`);
|
|
13240
|
-
const
|
|
13241
|
-
if (
|
|
13282
|
+
const format4 = consumer.outputs?.[0]?.format.toUpperCase();
|
|
13283
|
+
if (!positionalFormats.includes(format4)) return errors;
|
|
13242
13284
|
const hiddenKeys = keys.filter((key) => (consumer.fields ?? []).some((x2) => (x2.alias ?? x2.key) === key && x2.hidden));
|
|
13243
13285
|
if (hiddenKeys.length > 0)
|
|
13244
|
-
errors.push(`"split.by" of consumer "${consumer.name}" references hidden field(s) "${hiddenKeys.join(", ")}", which the ${
|
|
13286
|
+
errors.push(`"split.by" of consumer "${consumer.name}" references hidden field(s) "${hiddenKeys.join(", ")}", which the ${format4} output does not write \u2014 a hidden field can't be split on in a positional format.`);
|
|
13245
13287
|
return errors;
|
|
13246
13288
|
};
|
|
13247
13289
|
/**
|
|
13248
|
-
* Checks `omitNull`, which drops a record's null fields on export. It only means anything where
|
|
13249
|
-
* record carries its own field names
|
|
13250
|
-
*
|
|
13251
|
-
* column alignment. Both cases are rejected rather than ignored.
|
|
13290
|
+
* Checks `omitNull`, which drops a record's null fields on export. It only means anything where
|
|
13291
|
+
* a record carries its own field names: leaving a positional column out shifts every column
|
|
13292
|
+
* after it, so a positional output declaring it is rejected rather than ignored.
|
|
13252
13293
|
*/
|
|
13253
13294
|
this.validateOmitNull = (consumer) => {
|
|
13254
13295
|
const errors = [];
|
|
13255
|
-
const positionalFormats = ["CSV", "TXT", "PARQUET"];
|
|
13296
|
+
const positionalFormats = ["CSV", "FIXED", "TXT", "PARQUET"];
|
|
13256
13297
|
for (const output of (consumer.outputs ?? []).filter((x2) => x2.omitNull === true)) {
|
|
13257
13298
|
const format4 = output.format.toUpperCase();
|
|
13258
|
-
if (positionalFormats.includes(format4))
|
|
13299
|
+
if (positionalFormats.includes(format4))
|
|
13259
13300
|
errors.push(`The ${format4} output of consumer "${consumer.name}" declares "omitNull", which only applies to JSON/API output: a positional column can't be omitted from a row.`);
|
|
13260
|
-
continue;
|
|
13261
|
-
}
|
|
13262
|
-
const positional = (consumer.outputs ?? []).filter((x2) => positionalFormats.includes(x2.format.toUpperCase()));
|
|
13263
|
-
if (positional.length > 0)
|
|
13264
|
-
errors.push(`The ${format4} output of consumer "${consumer.name}" declares "omitNull", but the consumer also writes ${positional.map((x2) => x2.format.toUpperCase()).join(" and ")}. Every output is serialized from one record, so omitting fields would break the positional one \u2014 split them into two consumers.`);
|
|
13265
13301
|
}
|
|
13266
13302
|
return errors;
|
|
13267
13303
|
};
|
|
13268
13304
|
/**
|
|
13269
13305
|
* Checks `header`, which writes the field names as the first line of an exported file. A JSON
|
|
13270
|
-
* record carries its own field names
|
|
13271
|
-
* nothing there — rejected rather than ignored, so a config that
|
|
13272
|
-
* one.
|
|
13306
|
+
* record carries its own field names, a parquet file carries its own schema, and an API response
|
|
13307
|
+
* is not a file, so the flag means nothing there — rejected rather than ignored, so a config that
|
|
13308
|
+
* asks for a header always gets one.
|
|
13273
13309
|
*/
|
|
13274
13310
|
this.validateHeader = (consumer) => {
|
|
13275
13311
|
const errors = [];
|
|
13276
|
-
const
|
|
13312
|
+
const headerFormats = ["CSV", "TXT"];
|
|
13277
13313
|
for (const output of (consumer.outputs ?? []).filter((x2) => Algo_default.hasVal(x2.header))) {
|
|
13278
13314
|
const format4 = output.format.toUpperCase();
|
|
13279
|
-
if (!
|
|
13280
|
-
errors.push(`The ${format4} output of consumer "${consumer.name}" declares "header", which only applies to CSV/TXT output:
|
|
13315
|
+
if (!headerFormats.includes(format4))
|
|
13316
|
+
errors.push(`The ${format4} output of consumer "${consumer.name}" declares "header", which only applies to CSV/TXT output: ${this.headerlessReason(format4)}.`);
|
|
13281
13317
|
}
|
|
13282
13318
|
return errors;
|
|
13283
13319
|
};
|
|
13320
|
+
/** Why the format in hand has no header line, as the clause that finishes the sentence above. */
|
|
13321
|
+
this.headerlessReason = (format4) => {
|
|
13322
|
+
if (format4 === "FIXED") return "a positional line has no room for one, and a field name would not sit on the columns it describes";
|
|
13323
|
+
if (format4 === "PARQUET") return `a ${format4} file carries its own schema`;
|
|
13324
|
+
return `a ${format4} record already carries its own field names`;
|
|
13325
|
+
};
|
|
13326
|
+
/**
|
|
13327
|
+
* Checks a consumer's FIXED output from its configuration alone: the properties only a
|
|
13328
|
+
* positional file has, the ones it cannot have, and whether anything downstream tries to read
|
|
13329
|
+
* one back. The layout itself — widths, columns and record lengths — is resolved against the
|
|
13330
|
+
* dataset's columns, which the producers define, so it is checked by `compile` and again before
|
|
13331
|
+
* a run starts rather than here.
|
|
13332
|
+
*
|
|
13333
|
+
* A property declared on the wrong format is rejected rather than ignored, the way a `position`
|
|
13334
|
+
* on a non-FIXED producer is: silently dropping a rule the author wrote is how a file ships
|
|
13335
|
+
* looking almost right.
|
|
13336
|
+
*/
|
|
13337
|
+
this.validateFixedOutput = (consumer) => {
|
|
13338
|
+
Affirm_default(consumer, "Invalid consumer");
|
|
13339
|
+
const errors = [];
|
|
13340
|
+
const output = consumer.outputs?.[0];
|
|
13341
|
+
if (!output) return errors;
|
|
13342
|
+
const format4 = output.format?.toUpperCase();
|
|
13343
|
+
const positioned = (consumer.fields ?? []).filter((x2) => x2.position).map((x2) => `"${x2.alias ?? x2.key}"`);
|
|
13344
|
+
if (format4 !== "FIXED") {
|
|
13345
|
+
const fixedOnly = ["recordLength", "defaults", "lineEnding", "layouts"];
|
|
13346
|
+
const declared = fixedOnly.filter((name) => Algo_default.hasVal(output[name]));
|
|
13347
|
+
if (declared.length > 0)
|
|
13348
|
+
errors.push(`The ${format4} output of consumer "${consumer.name}" declares ${declared.map((x2) => `"${x2}"`).join(", ")}, which only ${declared.length > 1 ? "apply" : "applies"} to a FIXED output, where ${declared.length > 1 ? "they are" : "it is"} ignored.`);
|
|
13349
|
+
if (positioned.length > 0)
|
|
13350
|
+
errors.push(`Consumer "${consumer.name}" declares "position" on field(s) ${positioned.join(", ")} but its output format is not "FIXED", where positions are ignored.`);
|
|
13351
|
+
return errors;
|
|
13352
|
+
}
|
|
13353
|
+
if (Algo_default.hasVal(output.delimiter))
|
|
13354
|
+
errors.push(`The FIXED output of consumer "${consumer.name}" declares a "delimiter". A positional line has none: its fields are found by the columns they occupy.`);
|
|
13355
|
+
if ((consumer.fields ?? []).some((x2) => x2.key === "*"))
|
|
13356
|
+
errors.push(`Consumer "${consumer.name}" selects fields with the wildcard key "*", which a FIXED output can't write: every field of a positional line declares its own "width", so the field list has to be explicit.`);
|
|
13357
|
+
return errors;
|
|
13358
|
+
};
|
|
13284
13359
|
/**
|
|
13285
13360
|
* Checks a field whose key selects columns by pattern ("addr*Street"). A pattern that matches
|
|
13286
13361
|
* nothing is always a mistake — a renamed dimension, or a producer that never had those columns —
|
|
@@ -14861,7 +14936,7 @@ A consumer takes the data from one or more producers and changes it's shape to t
|
|
|
14861
14936
|
# RULES
|
|
14862
14937
|
- If a field is not needed, do not add it e.g.
|
|
14863
14938
|
- Only import a producer once
|
|
14864
|
-
- Awlays include this exact property as the first -> $schema": "https://raw.githubusercontent.com/ForzaLabs/remora-public/refs/heads/main/json_schemas/consumer-schema.json",
|
|
14939
|
+
- Awlays include this exact property as the first -> "$schema": "https://raw.githubusercontent.com/ForzaLabs/remora-public/refs/heads/main/json_schemas/consumer-schema.json",
|
|
14865
14940
|
- Use "API" as the only valid output format.
|
|
14866
14941
|
- The "from" must contain only the name of the producer
|
|
14867
14942
|
|
|
@@ -33669,6 +33744,96 @@ var init_CSVParser = __esm({
|
|
|
33669
33744
|
}
|
|
33670
33745
|
});
|
|
33671
33746
|
|
|
33747
|
+
// ../../packages/parsing/src/DatasetCodec.ts
|
|
33748
|
+
var NULL_FIELD, NEEDS_ESCAPE, DatasetCodecClass, DatasetCodec, DatasetCodec_default;
|
|
33749
|
+
var init_DatasetCodec = __esm({
|
|
33750
|
+
"../../packages/parsing/src/DatasetCodec.ts"() {
|
|
33751
|
+
init_src();
|
|
33752
|
+
NULL_FIELD = "";
|
|
33753
|
+
NEEDS_ESCAPE = /["\n\r]/;
|
|
33754
|
+
DatasetCodecClass = class {
|
|
33755
|
+
constructor() {
|
|
33756
|
+
/**
|
|
33757
|
+
* One value as an internal positional field: quoted, with `"`, `\n` and `\r` escaped - or the
|
|
33758
|
+
* unquoted null marker when there is no value.
|
|
33759
|
+
*/
|
|
33760
|
+
this.encodeField = (value) => {
|
|
33761
|
+
if (value === null || value === void 0) return NULL_FIELD;
|
|
33762
|
+
const text = typeof value === "string" ? value : String(value);
|
|
33763
|
+
if (!NEEDS_ESCAPE.test(text)) return `"${text}"`;
|
|
33764
|
+
return `"${text.replaceAll('"', '""').replaceAll("\r", "\\r").replaceAll("\n", "\\n")}"`;
|
|
33765
|
+
};
|
|
33766
|
+
/** One record as an internal positional line. The values are already in the schema's order. */
|
|
33767
|
+
this.encodeRow = (values, delimiter = ",") => {
|
|
33768
|
+
Affirm_default(values, "Invalid values");
|
|
33769
|
+
Affirm_default(delimiter, "Invalid delimiter");
|
|
33770
|
+
let row = "";
|
|
33771
|
+
for (let index = 0; index < values.length; index++) {
|
|
33772
|
+
if (index > 0)
|
|
33773
|
+
row += delimiter;
|
|
33774
|
+
row += this.encodeField(values[index]);
|
|
33775
|
+
}
|
|
33776
|
+
return row;
|
|
33777
|
+
};
|
|
33778
|
+
/**
|
|
33779
|
+
* One internal positional line split back into its raw fields, with a null still a null. The
|
|
33780
|
+
* array may be SHORTER than the schema's column list; a reader treats a missing entry as null.
|
|
33781
|
+
*/
|
|
33782
|
+
this.decodeRow = (line, delimiter = ",") => {
|
|
33783
|
+
Affirm_default(delimiter, "Invalid delimiter");
|
|
33784
|
+
const fields = [];
|
|
33785
|
+
const length = line?.length ?? 0;
|
|
33786
|
+
const delimiterCode = delimiter.charCodeAt(0);
|
|
33787
|
+
let fieldStart = 0;
|
|
33788
|
+
let fieldEnd = 0;
|
|
33789
|
+
let inQuotes = false;
|
|
33790
|
+
let hasQuotes = false;
|
|
33791
|
+
let index = 0;
|
|
33792
|
+
while (index < length) {
|
|
33793
|
+
const char = line.charCodeAt(index);
|
|
33794
|
+
if (char === 34) {
|
|
33795
|
+
if (!inQuotes) {
|
|
33796
|
+
inQuotes = true;
|
|
33797
|
+
hasQuotes = true;
|
|
33798
|
+
fieldStart = index + 1;
|
|
33799
|
+
} else if (line.charCodeAt(index + 1) === 34) {
|
|
33800
|
+
index++;
|
|
33801
|
+
} else {
|
|
33802
|
+
inQuotes = false;
|
|
33803
|
+
fieldEnd = index;
|
|
33804
|
+
}
|
|
33805
|
+
} else if (char === delimiterCode && !inQuotes) {
|
|
33806
|
+
fields.push(this._field(line, fieldStart, fieldEnd, hasQuotes));
|
|
33807
|
+
fieldStart = index + 1;
|
|
33808
|
+
fieldEnd = 0;
|
|
33809
|
+
hasQuotes = false;
|
|
33810
|
+
}
|
|
33811
|
+
index++;
|
|
33812
|
+
}
|
|
33813
|
+
fields.push(this._field(line, fieldStart, fieldEnd, hasQuotes));
|
|
33814
|
+
return fields;
|
|
33815
|
+
};
|
|
33816
|
+
/**
|
|
33817
|
+
* Re-encodes an internal line with another delimiter, which is what a `TXT` export is. Not via
|
|
33818
|
+
* `CSVParser`: a plain parse reads both the null marker and `""` as the empty string.
|
|
33819
|
+
*/
|
|
33820
|
+
this.redelimit = (line, delimiter) => {
|
|
33821
|
+
Affirm_default(delimiter, "Invalid delimiter");
|
|
33822
|
+
return this.encodeRow(this.decodeRow(line), delimiter);
|
|
33823
|
+
};
|
|
33824
|
+
/** One field's text, or null for the unquoted marker. Only a quoted token carries a value. */
|
|
33825
|
+
this._field = (line, start, end, hasQuotes) => {
|
|
33826
|
+
if (!hasQuotes) return null;
|
|
33827
|
+
const raw = line.slice(start, end);
|
|
33828
|
+
return raw.indexOf('"') === -1 ? raw : raw.replaceAll('""', '"');
|
|
33829
|
+
};
|
|
33830
|
+
}
|
|
33831
|
+
};
|
|
33832
|
+
DatasetCodec = new DatasetCodecClass();
|
|
33833
|
+
DatasetCodec_default = DatasetCodec;
|
|
33834
|
+
}
|
|
33835
|
+
});
|
|
33836
|
+
|
|
33672
33837
|
// ../../packages/parsing/src/FixedWidthParser.ts
|
|
33673
33838
|
var FixedWidthParserClass, FixedWidthParser, FixedWidthParser_default;
|
|
33674
33839
|
var init_FixedWidthParser = __esm({
|
|
@@ -33823,6 +33988,307 @@ var init_FixedWidthParser = __esm({
|
|
|
33823
33988
|
}
|
|
33824
33989
|
});
|
|
33825
33990
|
|
|
33991
|
+
// ../../packages/parsing/src/FixedWidthWriter.ts
|
|
33992
|
+
var BASE_FILL, TYPE_FILL, FixedWidthWriterClass, FixedWidthWriter, FixedWidthWriter_default;
|
|
33993
|
+
var init_FixedWidthWriter = __esm({
|
|
33994
|
+
"../../packages/parsing/src/FixedWidthWriter.ts"() {
|
|
33995
|
+
init_src();
|
|
33996
|
+
init_src3();
|
|
33997
|
+
BASE_FILL = { align: "left", pad: " ", nullFill: " ", overflow: "fail" };
|
|
33998
|
+
TYPE_FILL = {
|
|
33999
|
+
string: BASE_FILL,
|
|
34000
|
+
number: { ...BASE_FILL, align: "right", pad: "0" },
|
|
34001
|
+
datetime: BASE_FILL,
|
|
34002
|
+
boolean: BASE_FILL
|
|
34003
|
+
};
|
|
34004
|
+
FixedWidthWriterClass = class {
|
|
34005
|
+
constructor() {
|
|
34006
|
+
/**
|
|
34007
|
+
* Resolves one record layout for writing: every column's fill rules decided, its columns
|
|
34008
|
+
* computed, and the record length agreed with what the output declares.
|
|
34009
|
+
*
|
|
34010
|
+
* Called once per layout when an export starts, never per record. `columns` is the line as the
|
|
34011
|
+
* dataset schema has it — line order, visible fields only — and `positions` carries each one's
|
|
34012
|
+
* `position` block, keyed by the same name the column has.
|
|
34013
|
+
*
|
|
34014
|
+
* `context` is the phrase that names this layout in an error, e.g. `the FIXED output of consumer
|
|
34015
|
+
* "x"`, so a message reads as one sentence wherever the layout came from.
|
|
34016
|
+
*/
|
|
34017
|
+
this.compilePlan = (columns, positions, output, context = "the FIXED output") => {
|
|
34018
|
+
Affirm_default(output, "Invalid consumer output");
|
|
34019
|
+
const selected = columns?.map((column, index) => ({ column, index }));
|
|
34020
|
+
return this.plan(selected, positions, output.defaults, output.recordLength, context);
|
|
34021
|
+
};
|
|
34022
|
+
/**
|
|
34023
|
+
* A FIXED output resolved for INSPECTION rather than for writing: every problem with it, and the
|
|
34024
|
+
* layouts it would write. What `compile` reports and prints.
|
|
34025
|
+
*
|
|
34026
|
+
* The rules are not restated here. The layouts are resolved by the same code the export runs,
|
|
34027
|
+
* and what that throws is reported rather than raised — so a layout `compile` accepts is a
|
|
34028
|
+
* layout the export can write. What review adds is that every field's own declaration is
|
|
34029
|
+
* checked first, across the whole output: an author with six fields missing a width sees six
|
|
34030
|
+
* lines from one `compile`, not one line from each of six.
|
|
34031
|
+
*
|
|
34032
|
+
* A field named by a layout that the dataset has no column for is left to the resolution below,
|
|
34033
|
+
* which names it as the layout's error rather than as a field with no width.
|
|
34034
|
+
*/
|
|
34035
|
+
this.review = (dsSchema, positions, output, consumerName) => {
|
|
34036
|
+
Affirm_default(dsSchema, "Invalid dataset schema");
|
|
34037
|
+
Affirm_default(output, "Invalid consumer output");
|
|
34038
|
+
const context = `the FIXED output${consumerName ? ` of consumer "${consumerName}"` : ""}`;
|
|
34039
|
+
const declared = this.declarationErrors(dsSchema, positions, output, context);
|
|
34040
|
+
if (declared.length > 0) return { errors: declared, layouts: [] };
|
|
34041
|
+
try {
|
|
34042
|
+
if (!output.layouts)
|
|
34043
|
+
return { errors: [], layouts: [{ value: null, plan: this.compilePlan(dsSchema.columns, positions, output, context) }] };
|
|
34044
|
+
const layouts = this.compileLayouts(dsSchema, positions, output, consumerName);
|
|
34045
|
+
return { errors: [], layouts: layouts.order.map((value) => ({ value, plan: layouts.plans.get(value) })) };
|
|
34046
|
+
} catch (e7) {
|
|
34047
|
+
return { errors: [this.reason(e7)], layouts: [] };
|
|
34048
|
+
}
|
|
34049
|
+
};
|
|
34050
|
+
/** Every field's own declaration, over every field the output writes, before any offset is computed. */
|
|
34051
|
+
this.declarationErrors = (dsSchema, positions, output, context) => {
|
|
34052
|
+
const written = output.layouts ? [...new Set((output.layouts.order ?? []).flatMap((value) => output.layouts.values?.[value]?.fields ?? []))] : dsSchema.columns.map((column) => column.name);
|
|
34053
|
+
const errors = [];
|
|
34054
|
+
for (const name of written) {
|
|
34055
|
+
const column = dsSchema.columns.find((x2) => x2.name === name);
|
|
34056
|
+
if (column) errors.push(...this.fieldErrors(column, positions?.[name], output.defaults, context));
|
|
34057
|
+
}
|
|
34058
|
+
return errors;
|
|
34059
|
+
};
|
|
34060
|
+
/**
|
|
34061
|
+
* What an affirm said, as a sentence a report can start a line with: without the prefix it
|
|
34062
|
+
* carries for a log, and with its first letter upper-cased — the messages are written to be
|
|
34063
|
+
* embedded ("... of the FIXED output of consumer x ..."), so some begin with that context
|
|
34064
|
+
* rather than with a capital.
|
|
34065
|
+
*/
|
|
34066
|
+
this.reason = (error) => {
|
|
34067
|
+
const prefix2 = "Affirm failed: ";
|
|
34068
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
34069
|
+
const sentence = message.startsWith(prefix2) ? message.slice(prefix2.length) : message;
|
|
34070
|
+
return sentence.charAt(0).toUpperCase() + sentence.slice(1);
|
|
34071
|
+
};
|
|
34072
|
+
/**
|
|
34073
|
+
* The record layouts of a multi-layout FIXED output, compiled in `order` so the export writes
|
|
34074
|
+
* its blocks in the order the author declared.
|
|
34075
|
+
*
|
|
34076
|
+
* Each layout names a subset of the dataset's columns, in its own column order, so a field's
|
|
34077
|
+
* place on the written line has nothing to do with its place on the staged one — which is what
|
|
34078
|
+
* `IFixedWriteField.index` carries. The output's `defaults` stand under every layout's own.
|
|
34079
|
+
*/
|
|
34080
|
+
this.compileLayouts = (dsSchema, positions, output, consumerName) => {
|
|
34081
|
+
Affirm_default(dsSchema, "Invalid dataset schema");
|
|
34082
|
+
Affirm_default(output?.layouts, "Invalid fixed-width layouts");
|
|
34083
|
+
const { discriminator, order, values } = output.layouts;
|
|
34084
|
+
const where = consumerName ? ` of consumer "${consumerName}"` : "";
|
|
34085
|
+
Affirm_default(discriminator, `The FIXED layouts${where} name no discriminator field`);
|
|
34086
|
+
Affirm_default.hasItems(order, `The FIXED layouts${where} declare no "order", which is the order their record blocks are written in`);
|
|
34087
|
+
Affirm_default.noDupes(order, `The FIXED layouts${where} name a discriminator value twice in "order"`);
|
|
34088
|
+
Affirm_default(
|
|
34089
|
+
order.length <= Constants_default.defaults.MAX_FIXED_LAYOUTS,
|
|
34090
|
+
`The FIXED layouts${where} declare ${order.length} record layouts, more than the ${Constants_default.defaults.MAX_FIXED_LAYOUTS} one output may write`
|
|
34091
|
+
);
|
|
34092
|
+
Affirm_default(
|
|
34093
|
+
output.recordLength === void 0 || output.recordLength === null,
|
|
34094
|
+
`The FIXED output${where} declares both "layouts" and a "recordLength". Each layout declares its own; the output has no single one.`
|
|
34095
|
+
);
|
|
34096
|
+
const index = dsSchema.columns.findIndex((column) => column.name === discriminator);
|
|
34097
|
+
Affirm_default(index >= 0, `The FIXED discriminator "${discriminator}"${where} is not one of the consumer's visible fields`);
|
|
34098
|
+
const plans = /* @__PURE__ */ new Map();
|
|
34099
|
+
for (const value of order)
|
|
34100
|
+
plans.set(value, this.layoutPlan(values?.[value], dsSchema, positions, output, value, consumerName));
|
|
34101
|
+
return { discriminator, index, order, plans };
|
|
34102
|
+
};
|
|
34103
|
+
/** One entry of the `layouts` map as a plan: its named fields, found on the staged line, in its order. */
|
|
34104
|
+
this.layoutPlan = (layout, dsSchema, positions, output, value, consumerName) => {
|
|
34105
|
+
const of = consumerName ? ` of consumer "${consumerName}"` : "";
|
|
34106
|
+
const where = `the FIXED layout "${value}"${of}`;
|
|
34107
|
+
Affirm_default(layout, `The FIXED "order"${of} names the discriminator value "${value}", which has no layout under "values"`);
|
|
34108
|
+
Affirm_default.hasItems(layout.fields, `${where} writes no field`);
|
|
34109
|
+
Affirm_default.noDupes(layout.fields, `${where} writes a field twice`);
|
|
34110
|
+
const selected = layout.fields.map((name) => {
|
|
34111
|
+
const index = dsSchema.columns.findIndex((column) => column.name === name);
|
|
34112
|
+
Affirm_default(index >= 0, `The field "${name}" of ${where} is not one of the consumer's visible fields`);
|
|
34113
|
+
return { column: dsSchema.columns[index], index };
|
|
34114
|
+
});
|
|
34115
|
+
return this.plan(selected, positions, this.mergeDefaults(output.defaults, layout.defaults), layout.recordLength, where);
|
|
34116
|
+
};
|
|
34117
|
+
/**
|
|
34118
|
+
* One record layout ready to write: every column's fill rules decided, its columns computed from
|
|
34119
|
+
* the widths before it, and the record length agreed with what the layout declares.
|
|
34120
|
+
*/
|
|
34121
|
+
this.plan = (selected, positions, defaults, recordLength, context) => {
|
|
34122
|
+
Affirm_default.hasItems(selected, `${context} has no field to write`);
|
|
34123
|
+
Affirm_default(positions, "Invalid fixed-width field positions");
|
|
34124
|
+
const fields = [];
|
|
34125
|
+
let start = 1;
|
|
34126
|
+
for (const { column, index } of selected) {
|
|
34127
|
+
const field = this.resolveField(column, positions[column.name], defaults, start, context);
|
|
34128
|
+
fields.push({ ...field, index });
|
|
34129
|
+
start = field.end + 1;
|
|
34130
|
+
}
|
|
34131
|
+
const composed = start - 1;
|
|
34132
|
+
Affirm_default(
|
|
34133
|
+
recordLength === void 0 || recordLength === null || recordLength === composed,
|
|
34134
|
+
`${context} declares a recordLength of ${recordLength}, but its ${fields.length} field widths sum to ${composed}. A field, a filler or a width is missing.`
|
|
34135
|
+
);
|
|
34136
|
+
return { fields, recordLength: composed };
|
|
34137
|
+
};
|
|
34138
|
+
/**
|
|
34139
|
+
* The layout's per-type fill rules over the output's, type by type — so an output states a rule
|
|
34140
|
+
* once for the whole file and a layout only names where it differs.
|
|
34141
|
+
*/
|
|
34142
|
+
this.mergeDefaults = (output, layout) => {
|
|
34143
|
+
if (!layout) return output;
|
|
34144
|
+
if (!output) return layout;
|
|
34145
|
+
const merged = { ...output };
|
|
34146
|
+
for (const type of Object.keys(layout))
|
|
34147
|
+
merged[type] = this.override(output[type] ?? {}, layout[type]);
|
|
34148
|
+
return merged;
|
|
34149
|
+
};
|
|
34150
|
+
/**
|
|
34151
|
+
* The layout a record is written with, by the value of the discriminator field on its staged
|
|
34152
|
+
* line. A value with no layout stops the run: it is a record the file has no block for, and
|
|
34153
|
+
* writing it under another layout would put its fields at the wrong columns.
|
|
34154
|
+
*/
|
|
34155
|
+
this.planFor = (values, layouts) => {
|
|
34156
|
+
const value = values?.[layouts.index];
|
|
34157
|
+
const plan = value === null || value === void 0 ? null : layouts.plans.get(String(value));
|
|
34158
|
+
Affirm_default(plan, `No FIXED layout for the value ${value === null || value === void 0 ? "null" : `"${value}"`} of the discriminator field "${layouts.discriminator}". The layouts cover ${layouts.order.map((x2) => `"${x2}"`).join(", ")}.`);
|
|
34159
|
+
return plan;
|
|
34160
|
+
};
|
|
34161
|
+
/**
|
|
34162
|
+
* Everything wrong with the way ONE field is declared, as messages rather than as a throw: no
|
|
34163
|
+
* `position` at all, a width that is not a whole number of columns, a fill character that is not
|
|
34164
|
+
* one character.
|
|
34165
|
+
*
|
|
34166
|
+
* Not the field's place on the line — that is only meaningful once every field before it stands
|
|
34167
|
+
* on its own, so `resolveField` checks it where it computes it. This is the one statement of the
|
|
34168
|
+
* rules: the export affirms them, and `review` collects them for every field of an output so
|
|
34169
|
+
* `compile` reports six missing widths in one run rather than one per run.
|
|
34170
|
+
*/
|
|
34171
|
+
this.fieldErrors = (column, position, defaults, context) => {
|
|
34172
|
+
Affirm_default(column, "Invalid dataset column");
|
|
34173
|
+
const where = `field "${column.name}"${context ? ` of ${context}` : ""}`;
|
|
34174
|
+
if (!position)
|
|
34175
|
+
return [`Missing "position" on ${where}: every visible field of a FIXED output declares the width it occupies`];
|
|
34176
|
+
const errors = [];
|
|
34177
|
+
if (!Number.isInteger(position.width) || position.width < 1)
|
|
34178
|
+
errors.push(`"position.width" of ${where} must be a whole number of 1 or greater`);
|
|
34179
|
+
const fill2 = this.fillOf(column.type, position, defaults);
|
|
34180
|
+
if (fill2.pad.length !== 1)
|
|
34181
|
+
errors.push(`"position.pad" of ${where} must be a single character, not "${fill2.pad}"`);
|
|
34182
|
+
if (fill2.nullFill.length !== 1)
|
|
34183
|
+
errors.push(`"position.nullFill" of ${where} must be a single character, not "${fill2.nullFill}"`);
|
|
34184
|
+
return errors;
|
|
34185
|
+
};
|
|
34186
|
+
/**
|
|
34187
|
+
* One column with its fill rules resolved and its place on the line computed. The field's own
|
|
34188
|
+
* `position` wins over the output's `defaults` for its type, which wins over the type's own rule,
|
|
34189
|
+
* so a wide layout states a rule once and names only its exceptions.
|
|
34190
|
+
*/
|
|
34191
|
+
this.resolveField = (column, position, defaults, start, context) => {
|
|
34192
|
+
const declaration = this.fieldErrors(column, position, defaults, context);
|
|
34193
|
+
Affirm_default(declaration.length === 0, declaration.join(" "));
|
|
34194
|
+
const where = `field "${column.name}"${context ? ` of ${context}` : ""}`;
|
|
34195
|
+
const fill2 = this.fillOf(column.type, position, defaults);
|
|
34196
|
+
const end = start + position.width - 1;
|
|
34197
|
+
Affirm_default(
|
|
34198
|
+
position.start === void 0 || position.start === null || position.start === start,
|
|
34199
|
+
`"position.start" of ${where} says column ${position.start}, but the widths before it place the field at column ${start}. A field or a filler is missing, or two are out of order.`
|
|
34200
|
+
);
|
|
34201
|
+
return {
|
|
34202
|
+
name: column.name,
|
|
34203
|
+
width: position.width,
|
|
34204
|
+
start,
|
|
34205
|
+
end,
|
|
34206
|
+
align: fill2.align,
|
|
34207
|
+
pad: fill2.pad,
|
|
34208
|
+
nullText: fill2.nullFill.repeat(position.width),
|
|
34209
|
+
overflow: fill2.overflow
|
|
34210
|
+
};
|
|
34211
|
+
};
|
|
34212
|
+
/** The three fill sources folded into one, nearest declaration first. */
|
|
34213
|
+
this.fillOf = (type, position, defaults) => {
|
|
34214
|
+
const base = TYPE_FILL[type] ?? TYPE_FILL.string;
|
|
34215
|
+
return this.override(this.override(base, defaults?.[type]), position);
|
|
34216
|
+
};
|
|
34217
|
+
/** `fill` with whatever `override` actually declares. An absent rule leaves the one below it standing. */
|
|
34218
|
+
this.override = (fill2, override) => {
|
|
34219
|
+
if (!override) return fill2;
|
|
34220
|
+
const merged = { ...fill2 };
|
|
34221
|
+
if (override.align) merged.align = override.align;
|
|
34222
|
+
if (override.pad) merged.pad = override.pad;
|
|
34223
|
+
if (override.nullFill) merged.nullFill = override.nullFill;
|
|
34224
|
+
if (override.overflow) merged.overflow = override.overflow;
|
|
34225
|
+
return merged;
|
|
34226
|
+
};
|
|
34227
|
+
/**
|
|
34228
|
+
* One record as its positional line, with no delimiter, no quoting and no terminator. The values
|
|
34229
|
+
* are the record's fields as the STAGED line holds them, each plan field reading the one it was
|
|
34230
|
+
* compiled against; a value the array does not reach is written as a null, which is what a
|
|
34231
|
+
* staged line does when its last fields carry nothing.
|
|
34232
|
+
*/
|
|
34233
|
+
this.composeLine = (values, plan) => {
|
|
34234
|
+
Affirm_default(values, "Invalid record values");
|
|
34235
|
+
Affirm_default(plan, "Invalid fixed-width write plan");
|
|
34236
|
+
let line = "";
|
|
34237
|
+
for (const field of plan.fields)
|
|
34238
|
+
line += this.writeField(values[field.index], field);
|
|
34239
|
+
Affirm_default(line.length === plan.recordLength, `A fixed-width record composed to ${line.length} characters, not the declared recordLength of ${plan.recordLength}`);
|
|
34240
|
+
return line;
|
|
34241
|
+
};
|
|
34242
|
+
/**
|
|
34243
|
+
* One value as one field: its text padded out to the field's width on the side `align` names.
|
|
34244
|
+
*
|
|
34245
|
+
* A null is the full width in `nullFill`, and an EMPTY STRING is a null — the staged encoding
|
|
34246
|
+
* keeps the two apart and a positional line cannot. A value too long for its field stops the
|
|
34247
|
+
* run; see `overflowed`.
|
|
34248
|
+
*/
|
|
34249
|
+
this.writeField = (value, field) => {
|
|
34250
|
+
if (value === null || value === void 0) return field.nullText;
|
|
34251
|
+
const text = typeof value === "string" ? value : String(value);
|
|
34252
|
+
if (text.length === 0) return field.nullText;
|
|
34253
|
+
if (text.length > field.width) return this.overflowed(text, field);
|
|
34254
|
+
return field.align === "right" ? text.padStart(field.width, field.pad) : text.padEnd(field.width, field.pad);
|
|
34255
|
+
};
|
|
34256
|
+
/**
|
|
34257
|
+
* A value longer than its field. It fails the run by default: a truncated account number is a
|
|
34258
|
+
* plausible-looking account number, and silent corruption in a feed nobody re-reads is the one
|
|
34259
|
+
* failure mode a positional writer must not have.
|
|
34260
|
+
*
|
|
34261
|
+
* Where the layout genuinely specifies a cut, the end that survives follows `align`, which is
|
|
34262
|
+
* what a COBOL MOVE does: a left-aligned value keeps its head, a right-aligned one keeps its
|
|
34263
|
+
* low-order tail.
|
|
34264
|
+
*/
|
|
34265
|
+
this.overflowed = (text, field) => {
|
|
34266
|
+
Affirm_default(
|
|
34267
|
+
field.overflow === "truncate",
|
|
34268
|
+
`Value "${text}" is ${text.length} characters, longer than the ${field.width} of field "${field.name}" at columns ${field.start}-${field.end}. Widen the field, or set its "overflow" to "truncate" if the layout really specifies a cut.`
|
|
34269
|
+
);
|
|
34270
|
+
return field.align === "right" ? text.slice(text.length - field.width) : text.slice(0, field.width);
|
|
34271
|
+
};
|
|
34272
|
+
/**
|
|
34273
|
+
* What a record carries after its last field, BEFORE the newline the destination writes. Every
|
|
34274
|
+
* destination driver ends each transformed line with one `\n`, the last line included, so an
|
|
34275
|
+
* `lf` output needs nothing here and a `crlf` one needs the `\r` that turns that newline into a
|
|
34276
|
+
* CRLF — which is exactly what a CRLF file looks like to anything that splits on `\n`.
|
|
34277
|
+
*
|
|
34278
|
+
* A real positional feed picks one terminator and holds it: the NASCO POD files are CRLF on
|
|
34279
|
+
* every line including the last, and `lf` is the default everywhere else.
|
|
34280
|
+
*/
|
|
34281
|
+
this.lineSuffix = (output) => {
|
|
34282
|
+
Affirm_default(output, "Invalid consumer output");
|
|
34283
|
+
return output.lineEnding === "crlf" ? "\r" : "";
|
|
34284
|
+
};
|
|
34285
|
+
}
|
|
34286
|
+
};
|
|
34287
|
+
FixedWidthWriter = new FixedWidthWriterClass();
|
|
34288
|
+
FixedWidthWriter_default = FixedWidthWriter;
|
|
34289
|
+
}
|
|
34290
|
+
});
|
|
34291
|
+
|
|
33826
34292
|
// ../../node_modules/pend/index.js
|
|
33827
34293
|
var require_pend = __commonJS({
|
|
33828
34294
|
"../../node_modules/pend/index.js"(exports2, module2) {
|
|
@@ -33881,7 +34347,7 @@ var require_pend = __commonJS({
|
|
|
33881
34347
|
// ../../node_modules/yauzl/fd-slicer.js
|
|
33882
34348
|
var require_fd_slicer = __commonJS({
|
|
33883
34349
|
"../../node_modules/yauzl/fd-slicer.js"(exports2) {
|
|
33884
|
-
var
|
|
34350
|
+
var fs33 = require("fs");
|
|
33885
34351
|
var util = require("util");
|
|
33886
34352
|
var stream = require("stream");
|
|
33887
34353
|
var Readable10 = stream.Readable;
|
|
@@ -33901,7 +34367,7 @@ var require_fd_slicer = __commonJS({
|
|
|
33901
34367
|
FdSlicer.prototype.read = function(buffer, offset, length, position, callback) {
|
|
33902
34368
|
var self2 = this;
|
|
33903
34369
|
self2.pend.go(function(cb) {
|
|
33904
|
-
|
|
34370
|
+
fs33.read(self2.fd, buffer, offset, length, position, function(err2, bytesRead, buffer2) {
|
|
33905
34371
|
cb();
|
|
33906
34372
|
callback(err2, bytesRead, buffer2);
|
|
33907
34373
|
});
|
|
@@ -33918,7 +34384,7 @@ var require_fd_slicer = __commonJS({
|
|
|
33918
34384
|
self2.refCount -= 1;
|
|
33919
34385
|
if (self2.refCount < 0) throw new Error("invalid unref");
|
|
33920
34386
|
if (self2.refCount > 0) return;
|
|
33921
|
-
|
|
34387
|
+
fs33.close(self2.fd, onCloseDone);
|
|
33922
34388
|
function onCloseDone(err2) {
|
|
33923
34389
|
if (err2) {
|
|
33924
34390
|
self2.emit("error", err2);
|
|
@@ -33950,7 +34416,7 @@ var require_fd_slicer = __commonJS({
|
|
|
33950
34416
|
}
|
|
33951
34417
|
self2.context.pend.go(function(cb) {
|
|
33952
34418
|
var buffer = Buffer.allocUnsafe(toRead);
|
|
33953
|
-
|
|
34419
|
+
fs33.read(self2.context.fd, buffer, 0, toRead, self2.pos, function(err2, bytesRead) {
|
|
33954
34420
|
if (err2) {
|
|
33955
34421
|
self2.destroy(err2);
|
|
33956
34422
|
} else if (bytesRead === 0) {
|
|
@@ -34310,7 +34776,7 @@ var require_crc32 = __commonJS({
|
|
|
34310
34776
|
// ../../node_modules/yauzl/index.js
|
|
34311
34777
|
var require_yauzl = __commonJS({
|
|
34312
34778
|
"../../node_modules/yauzl/index.js"(exports2) {
|
|
34313
|
-
var
|
|
34779
|
+
var fs33 = require("fs");
|
|
34314
34780
|
var zlib3 = require("zlib");
|
|
34315
34781
|
var fd_slicer = require_fd_slicer();
|
|
34316
34782
|
var util = require("util");
|
|
@@ -34379,10 +34845,10 @@ var require_yauzl = __commonJS({
|
|
|
34379
34845
|
if (options2.validateEntrySizes == null) options2.validateEntrySizes = true;
|
|
34380
34846
|
if (options2.strictFileNames == null) options2.strictFileNames = false;
|
|
34381
34847
|
if (callback == null) callback = defaultCallback;
|
|
34382
|
-
|
|
34848
|
+
fs33.open(path22, "r", function(err2, fd) {
|
|
34383
34849
|
if (err2) return callback(err2);
|
|
34384
34850
|
fromFd(fd, options2, function(err3, zipfile) {
|
|
34385
|
-
if (err3)
|
|
34851
|
+
if (err3) fs33.close(fd, defaultCallback);
|
|
34386
34852
|
callback(err3, zipfile);
|
|
34387
34853
|
});
|
|
34388
34854
|
});
|
|
@@ -34399,7 +34865,7 @@ var require_yauzl = __commonJS({
|
|
|
34399
34865
|
if (options2.validateEntrySizes == null) options2.validateEntrySizes = true;
|
|
34400
34866
|
if (options2.strictFileNames == null) options2.strictFileNames = false;
|
|
34401
34867
|
if (callback == null) callback = defaultCallback;
|
|
34402
|
-
|
|
34868
|
+
fs33.fstat(fd, function(err2, stats) {
|
|
34403
34869
|
if (err2) return callback(err2);
|
|
34404
34870
|
var reader = new fd_slicer.FdSlicer(fd);
|
|
34405
34871
|
fromRandomAccessReader(reader, stats.size, options2, callback);
|
|
@@ -38478,7 +38944,9 @@ var init_XMLParser = __esm({
|
|
|
38478
38944
|
var init_src5 = __esm({
|
|
38479
38945
|
"../../packages/parsing/src/index.ts"() {
|
|
38480
38946
|
init_CSVParser();
|
|
38947
|
+
init_DatasetCodec();
|
|
38481
38948
|
init_FixedWidthParser();
|
|
38949
|
+
init_FixedWidthWriter();
|
|
38482
38950
|
init_ParseCompression();
|
|
38483
38951
|
init_ParseHelper();
|
|
38484
38952
|
init_XLSParser();
|
|
@@ -46114,8 +46582,8 @@ var init_LocalSourceDriver = __esm({
|
|
|
46114
46582
|
Logger_default.log(`Matched ${allFileKeys.length} files, copying to locally and creating unified dataset.`);
|
|
46115
46583
|
const firstPath = import_path10.default.join(this._path, allFileKeys[0]);
|
|
46116
46584
|
const headerLine = (await DriverHelper_default.quickReadFile(firstPath, 1))[0];
|
|
46117
|
-
const
|
|
46118
|
-
const results = await Promise.all(
|
|
46585
|
+
const promises3 = allFileKeys.map((x2, i6) => this._get({ ...request, fileKey: x2 }, headerLine, i6));
|
|
46586
|
+
const results = await Promise.all(promises3);
|
|
46119
46587
|
return results.flat();
|
|
46120
46588
|
} else {
|
|
46121
46589
|
return await this._get(request, "");
|
|
@@ -46135,8 +46603,8 @@ var init_LocalSourceDriver = __esm({
|
|
|
46135
46603
|
Logger_default.log(`Matched ${allFileKeys.length} files, copying to locally and creating unified dataset.`);
|
|
46136
46604
|
const firstPath = import_path10.default.join(this._path, allFileKeys[0]);
|
|
46137
46605
|
const headerLine = (await DriverHelper_default.quickReadFile(firstPath, 1))[0];
|
|
46138
|
-
const
|
|
46139
|
-
const results = await Promise.all(
|
|
46606
|
+
const promises3 = allFileKeys.map((x2, i6) => this._get({ ...request, fileKey: x2 }, headerLine, i6));
|
|
46607
|
+
const results = await Promise.all(promises3);
|
|
46140
46608
|
return results.flat();
|
|
46141
46609
|
} else {
|
|
46142
46610
|
return await this._get(request, "");
|
|
@@ -46564,8 +47032,8 @@ var init_S3SourceDriver = __esm({
|
|
|
46564
47032
|
if (fileKey.includes("%")) {
|
|
46565
47033
|
const allFileKeys = await this.listFiles(fileKey);
|
|
46566
47034
|
Affirm_default(allFileKeys.length < 50, `Pattern ${fileKey} of producer requested to S3 matches more than 50 files (${allFileKeys.length}), this is more than the S3 allowed limit. Please refine your pattern, remove some files or use a separate bucket.`);
|
|
46567
|
-
const
|
|
46568
|
-
const results = await Promise.all(
|
|
47035
|
+
const promises3 = allFileKeys.map((x2, i6) => this._get({ ...request, fileKey: x2 }, i6));
|
|
47036
|
+
const results = await Promise.all(promises3);
|
|
46569
47037
|
return results.flat();
|
|
46570
47038
|
} else {
|
|
46571
47039
|
return await this._get(request);
|
|
@@ -46578,8 +47046,8 @@ var init_S3SourceDriver = __esm({
|
|
|
46578
47046
|
const { fileKey } = request;
|
|
46579
47047
|
if (fileKey.includes("%")) {
|
|
46580
47048
|
const allFileKeys = await this.listFiles(fileKey);
|
|
46581
|
-
const
|
|
46582
|
-
const results = await Promise.all(
|
|
47049
|
+
const promises3 = allFileKeys.map((x2, i6) => this._get({ ...request, fileKey: x2 }, i6));
|
|
47050
|
+
const results = await Promise.all(promises3);
|
|
46583
47051
|
return results.flat();
|
|
46584
47052
|
} else {
|
|
46585
47053
|
return await this._get(request);
|
|
@@ -47526,7 +47994,7 @@ var init_ProducerManager = __esm({
|
|
|
47526
47994
|
});
|
|
47527
47995
|
|
|
47528
47996
|
// ../../packages/engines/src/transform/TypeCaster.ts
|
|
47529
|
-
var import_dayjs, import_customParseFormat, import_utc, DAY_OF_YEAR_TOKEN, FORMAT_ONLY_TOKENS, DATE_PARSE_TOKENS, TYPE_DEFAULTS, TypeCasterClass, TypeCaster, TypeCaster_default;
|
|
47997
|
+
var import_dayjs, import_customParseFormat, import_utc, DAY_OF_YEAR_TOKEN, FORMAT_ONLY_TOKENS, DATE_PARSE_TOKENS, TRUE_VALUES, TYPE_DEFAULTS, TypeCasterClass, TypeCaster, TypeCaster_default;
|
|
47530
47998
|
var init_TypeCaster = __esm({
|
|
47531
47999
|
"../../packages/engines/src/transform/TypeCaster.ts"() {
|
|
47532
48000
|
init_src();
|
|
@@ -47547,6 +48015,7 @@ var init_TypeCaster = __esm({
|
|
|
47547
48015
|
{ token: "mm", width: 2 },
|
|
47548
48016
|
{ token: "ss", width: 2 }
|
|
47549
48017
|
];
|
|
48018
|
+
TRUE_VALUES = /^(1|true|yes|y|t)$/i;
|
|
47550
48019
|
TYPE_DEFAULTS = {
|
|
47551
48020
|
string: "",
|
|
47552
48021
|
number: 0,
|
|
@@ -47556,7 +48025,8 @@ var init_TypeCaster = __esm({
|
|
|
47556
48025
|
TypeCasterClass = class {
|
|
47557
48026
|
constructor() {
|
|
47558
48027
|
/**
|
|
47559
|
-
* Casts a value for a DECLARED
|
|
48028
|
+
* Casts a value for a DECLARED field: the type cast, plus what a MISSING value means for it. The
|
|
48029
|
+
* field is a producer dimension on the read path and a dataset column on the re-read path.
|
|
47560
48030
|
*
|
|
47561
48031
|
* Missing is null/undefined, and — for every type but `string` — the empty string, which is how
|
|
47562
48032
|
* a NULL arrives from every source that stages through CSV (SQL, Delta Share, CSV files) and is
|
|
@@ -47588,6 +48058,7 @@ var init_TypeCaster = __esm({
|
|
|
47588
48058
|
switch (type) {
|
|
47589
48059
|
case "boolean": {
|
|
47590
48060
|
if (typeof value === "boolean") return value;
|
|
48061
|
+
if (typeof value === "string") return TRUE_VALUES.test(value.trim());
|
|
47591
48062
|
return Boolean(value);
|
|
47592
48063
|
}
|
|
47593
48064
|
case "datetime":
|
|
@@ -47726,6 +48197,7 @@ var init_TypeCaster = __esm({
|
|
|
47726
48197
|
var LineParserClass, LineParser, LineParser_default;
|
|
47727
48198
|
var init_LineParser = __esm({
|
|
47728
48199
|
"../../packages/engines/src/parsing/LineParser.ts"() {
|
|
48200
|
+
init_src();
|
|
47729
48201
|
init_TypeCaster();
|
|
47730
48202
|
init_src5();
|
|
47731
48203
|
LineParserClass = class {
|
|
@@ -47818,22 +48290,23 @@ var init_LineParser = __esm({
|
|
|
47818
48290
|
return record;
|
|
47819
48291
|
};
|
|
47820
48292
|
/**
|
|
47821
|
-
*
|
|
48293
|
+
* The parser of one staged `.dataset`, compiled once from the schema that file was written with.
|
|
48294
|
+
* The schema is the ONLY description of the file - it has no header - and there is deliberately
|
|
48295
|
+
* no overload taking a consumer's field list, which cannot know about a shape a pass changed.
|
|
47822
48296
|
*/
|
|
47823
|
-
this.
|
|
47824
|
-
|
|
47825
|
-
|
|
47826
|
-
|
|
47827
|
-
|
|
47828
|
-
|
|
47829
|
-
|
|
47830
|
-
|
|
47831
|
-
|
|
47832
|
-
|
|
47833
|
-
|
|
47834
|
-
|
|
47835
|
-
|
|
47836
|
-
return JSON.parse(line);
|
|
48297
|
+
this.datasetParser = (dsSchema) => {
|
|
48298
|
+
Affirm_default(dsSchema, "Invalid dataset schema");
|
|
48299
|
+
Affirm_default.hasItems(dsSchema.columns, "The dataset schema declares no column to read");
|
|
48300
|
+
if (dsSchema.encoding === "JSON")
|
|
48301
|
+
return (line) => JSON.parse(line);
|
|
48302
|
+
const { columns } = dsSchema;
|
|
48303
|
+
return (line) => {
|
|
48304
|
+
const parts = DatasetCodec_default.decodeRow(line);
|
|
48305
|
+
const record = {};
|
|
48306
|
+
for (let index = 0; index < columns.length; index++)
|
|
48307
|
+
record[columns[index].name] = TypeCaster_default.castDimension(parts[index], columns[index]);
|
|
48308
|
+
return record;
|
|
48309
|
+
};
|
|
47837
48310
|
};
|
|
47838
48311
|
}
|
|
47839
48312
|
};
|
|
@@ -47950,7 +48423,7 @@ var init_ProducerEngine = __esm({
|
|
|
47950
48423
|
let effectiveProducer = producer;
|
|
47951
48424
|
let effectiveSource = source;
|
|
47952
48425
|
if (compressionType) {
|
|
47953
|
-
scope = { id: (0, import_crypto6.randomUUID)(), folder: `sample-${(0, import_crypto6.randomUUID)()}`, workersId: [] };
|
|
48426
|
+
scope = ExecutorScope_default.open({ id: (0, import_crypto6.randomUUID)(), folder: `sample-${(0, import_crypto6.randomUUID)()}`, workersId: [] });
|
|
47954
48427
|
const driver = await DriverFactory_default.instantiateSource(source);
|
|
47955
48428
|
const readyResult = await driver.ready({ producer, scope });
|
|
47956
48429
|
const firstFile = readyResult.files[0].fullUri;
|
|
@@ -47960,7 +48433,7 @@ var init_ProducerEngine = __esm({
|
|
|
47960
48433
|
...producer,
|
|
47961
48434
|
settings: {
|
|
47962
48435
|
...producer.settings,
|
|
47963
|
-
fileKey: readyResult.files.length > 1 ? "%.
|
|
48436
|
+
fileKey: readyResult.files.length > 1 ? "%.extract" : import_path14.default.basename(firstFile),
|
|
47964
48437
|
compressionType: void 0
|
|
47965
48438
|
}
|
|
47966
48439
|
};
|
|
@@ -51213,8 +51686,7 @@ var init_ConsumerManager = __esm({
|
|
|
51213
51686
|
from: x2.owner
|
|
51214
51687
|
},
|
|
51215
51688
|
finalKey: x2.nameInProducer,
|
|
51216
|
-
dimension: x2.dimension
|
|
51217
|
-
measure: x2.measure
|
|
51689
|
+
dimension: x2.dimension
|
|
51218
51690
|
})));
|
|
51219
51691
|
} else {
|
|
51220
51692
|
const from = field.cField.from ?? (consumer.producers.length === 1 ? consumer.producers[0].name : null);
|
|
@@ -51226,8 +51698,7 @@ var init_ConsumerManager = __esm({
|
|
|
51226
51698
|
from: col.owner
|
|
51227
51699
|
},
|
|
51228
51700
|
finalKey: col.nameInProducer,
|
|
51229
|
-
dimension: col.dimension
|
|
51230
|
-
measure: col.measure
|
|
51701
|
+
dimension: col.dimension
|
|
51231
51702
|
});
|
|
51232
51703
|
});
|
|
51233
51704
|
}
|
|
@@ -51242,13 +51713,12 @@ var init_ConsumerManager = __esm({
|
|
|
51242
51713
|
from: field.cField.from ?? match.column.owner
|
|
51243
51714
|
},
|
|
51244
51715
|
finalKey: match.name,
|
|
51245
|
-
dimension: match.column.dimension
|
|
51246
|
-
measure: match.column.measure
|
|
51716
|
+
dimension: match.column.dimension
|
|
51247
51717
|
})));
|
|
51248
51718
|
} else {
|
|
51249
51719
|
const col = ConsumerManager.searchFieldInColumns(field.cField, availableColumns, consumer);
|
|
51250
51720
|
Affirm_default(col, `Consumer "${consumer.name}" misconfiguration: the requested field "${field.cField.key}" is not found in any of the specified producers ("${consumer.producers.map((x2) => x2.name).join(", ")}")`);
|
|
51251
|
-
expandedFields.push({ ...field, dimension: col.dimension
|
|
51721
|
+
expandedFields.push({ ...field, dimension: col.dimension });
|
|
51252
51722
|
}
|
|
51253
51723
|
return expandedFields;
|
|
51254
51724
|
};
|
|
@@ -52310,10 +52780,9 @@ var init_FieldLineage = __esm({
|
|
|
52310
52780
|
this._fromProducer = (producer, field) => {
|
|
52311
52781
|
const node = LineageEngine_default.nodeId("producer", producer.name);
|
|
52312
52782
|
const dimension = (producer.dimensions ?? []).find((x2) => x2.name === field.key);
|
|
52313
|
-
const
|
|
52314
|
-
const applied = dimension?.alias ? [`source column "${dimension.alias}"`] : measure ? ["measure"] : [];
|
|
52783
|
+
const applied = dimension?.alias ? [`source column "${dimension.alias}"`] : [];
|
|
52315
52784
|
const step = { resource: producer.name, node, field: field.key, applied };
|
|
52316
|
-
if (dimension
|
|
52785
|
+
if (dimension) return { path: [step] };
|
|
52317
52786
|
if (field.key === "*") return { path: [{ ...step, applied: ["every column of this producer"] }] };
|
|
52318
52787
|
if (Algo_default.isGlob(field.key)) return { path: [{ ...step, applied: ["every matching column"] }] };
|
|
52319
52788
|
return { path: [step], unresolved: `"${field.key}" is not a column of producer "${producer.name}"` };
|
|
@@ -52359,11 +52828,8 @@ var init_FieldLineage = __esm({
|
|
|
52359
52828
|
...field.hidden ? ["hidden from the export"] : []
|
|
52360
52829
|
];
|
|
52361
52830
|
};
|
|
52362
|
-
/** Every column a producer exposes,
|
|
52363
|
-
this._producerColumns = (producer) => [
|
|
52364
|
-
...(producer.dimensions ?? []).map((x2) => x2.name),
|
|
52365
|
-
...(producer.measures ?? []).map((x2) => x2.name)
|
|
52366
|
-
];
|
|
52831
|
+
/** Every column a producer exposes, in declared order. */
|
|
52832
|
+
this._producerColumns = (producer) => (producer.dimensions ?? []).map((x2) => x2.name);
|
|
52367
52833
|
/** Whether a resource exposes a column by that name. A `*` consumer exposes whatever it is asked. */
|
|
52368
52834
|
this._exposes = (env2, name, column) => {
|
|
52369
52835
|
const producer = (env2.producers ?? []).find((x2) => x2.name === name);
|
|
@@ -52694,7 +53160,6 @@ var init_ResourceView = __esm({
|
|
|
52694
53160
|
],
|
|
52695
53161
|
tables: [
|
|
52696
53162
|
this._dimensionsTable(producer),
|
|
52697
|
-
...producer.measures?.length ? [this._measuresTable(producer)] : [],
|
|
52698
53163
|
this._usedByTable(env2, producer),
|
|
52699
53164
|
this._keyValueTable("Settings", settings)
|
|
52700
53165
|
],
|
|
@@ -52731,16 +53196,6 @@ var init_ResourceView = __esm({
|
|
|
52731
53196
|
]);
|
|
52732
53197
|
return this._table("Dimensions", columns, rows);
|
|
52733
53198
|
};
|
|
52734
|
-
this._measuresTable = (producer) => {
|
|
52735
|
-
const rows = (producer.measures ?? []).map((measure) => [
|
|
52736
|
-
{ value: measure.name, note: this._note(measure.description) },
|
|
52737
|
-
{ value: measure.sql }
|
|
52738
|
-
]);
|
|
52739
|
-
return this._table("Measures", [
|
|
52740
|
-
{ key: "name", label: "Name" },
|
|
52741
|
-
{ key: "sql", label: "SQL" }
|
|
52742
|
-
], rows);
|
|
52743
|
-
};
|
|
52744
53199
|
/**
|
|
52745
53200
|
* Which consumers read each column, the downstream half of field-level lineage. It is the answer
|
|
52746
53201
|
* to "can this column go", which is a question a wide layout raises constantly and which nothing
|
|
@@ -54986,6 +55441,145 @@ var init_RunOverlay = __esm({
|
|
|
54986
55441
|
}
|
|
54987
55442
|
});
|
|
54988
55443
|
|
|
55444
|
+
// ../../packages/engines/src/parsing/ParquetExporter.ts
|
|
55445
|
+
var fs24, PARQUET_TYPES, TRUE_VALUES2, ParquetExporterClass, ParquetExporter, ParquetExporter_default;
|
|
55446
|
+
var init_ParquetExporter = __esm({
|
|
55447
|
+
"../../packages/engines/src/parsing/ParquetExporter.ts"() {
|
|
55448
|
+
fs24 = __toESM(require("fs"), 1);
|
|
55449
|
+
init_src();
|
|
55450
|
+
init_src3();
|
|
55451
|
+
init_src5();
|
|
55452
|
+
init_src2();
|
|
55453
|
+
PARQUET_TYPES = {
|
|
55454
|
+
string: "STRING",
|
|
55455
|
+
number: "DOUBLE",
|
|
55456
|
+
boolean: "BOOLEAN",
|
|
55457
|
+
datetime: "TIMESTAMP"
|
|
55458
|
+
};
|
|
55459
|
+
TRUE_VALUES2 = /^(1|true|yes|y|t)$/i;
|
|
55460
|
+
ParquetExporterClass = class {
|
|
55461
|
+
constructor() {
|
|
55462
|
+
/**
|
|
55463
|
+
* Writes `request.sourcePath` to `request.targetPath` as parquet, SNAPPY compressed, one typed
|
|
55464
|
+
* column per visible consumer field.
|
|
55465
|
+
*
|
|
55466
|
+
* Peak memory is bounded by one row group rather than by the dataset: the writer pulls the rows
|
|
55467
|
+
* a group at a time from the generator below, and nothing holds the file as a whole.
|
|
55468
|
+
*/
|
|
55469
|
+
this.exportToParquet = async (request) => {
|
|
55470
|
+
Affirm_default(request, "Missing required parameter request");
|
|
55471
|
+
Affirm_default(request.sourcePath, "Missing required parameter request.sourcePath");
|
|
55472
|
+
Affirm_default(request.targetPath, "Missing required parameter request.targetPath");
|
|
55473
|
+
Affirm_default(request.dsSchema, "Missing required parameter request.dsSchema");
|
|
55474
|
+
Affirm_default(request.consumerName, "Missing required parameter request.consumerName");
|
|
55475
|
+
const columns = this._columns(request.dsSchema);
|
|
55476
|
+
Affirm_default.hasItems(columns, `Consumer "${request.consumerName}" has no visible field to export as parquet`);
|
|
55477
|
+
const { parquetWriteRows, fileWriter } = await import("hyparquet-writer");
|
|
55478
|
+
const rowGroupSize = this._rowGroupSize(columns.length);
|
|
55479
|
+
const startedMs = Date.now();
|
|
55480
|
+
await parquetWriteRows({
|
|
55481
|
+
writer: fileWriter(request.targetPath),
|
|
55482
|
+
columns,
|
|
55483
|
+
rows: this._readRecords(request, columns),
|
|
55484
|
+
rowGroupSize
|
|
55485
|
+
});
|
|
55486
|
+
const bytes = fs24.statSync(request.targetPath).size;
|
|
55487
|
+
Logger_default.info(`Wrote parquet "${request.targetPath}" for consumer "${request.consumerName}" (${columns.length} column(s), ${rowGroupSize} rows/group, ${bytes} bytes, ${Date.now() - startedMs}ms)`);
|
|
55488
|
+
};
|
|
55489
|
+
/**
|
|
55490
|
+
* The exported schema, taken from the dataset's own: one column per column ON the line, in line
|
|
55491
|
+
* order. Not from the consumer's fields, which cannot know about a shape a `pivot` changed.
|
|
55492
|
+
*/
|
|
55493
|
+
this._columns = (dsSchema) => {
|
|
55494
|
+
return dsSchema.columns.map((column) => ({
|
|
55495
|
+
name: column.name,
|
|
55496
|
+
type: PARQUET_TYPES[column.type] ?? "STRING",
|
|
55497
|
+
nullable: true
|
|
55498
|
+
}));
|
|
55499
|
+
};
|
|
55500
|
+
/**
|
|
55501
|
+
* Rows per row group, derived from a budget of VALUES rather than a fixed row count: a row group
|
|
55502
|
+
* is buffered whole before it can be compressed, so what bounds the export's memory is cells, and
|
|
55503
|
+
* 100,000 rows means 400k values on a 4-column consumer and 130 million on a 1,300-field one.
|
|
55504
|
+
* Clamped at both ends — see the PARQUET_ constants for why each bound is there.
|
|
55505
|
+
*/
|
|
55506
|
+
this._rowGroupSize = (columnCount) => {
|
|
55507
|
+
Affirm_default(columnCount > 0, "Cannot size a row group for zero columns");
|
|
55508
|
+
const { PARQUET_ROW_GROUP_CELLS, PARQUET_MIN_ROW_GROUP_ROWS, PARQUET_MAX_ROW_GROUP_ROWS } = Constants_default.defaults;
|
|
55509
|
+
const rows = Math.floor(PARQUET_ROW_GROUP_CELLS / columnCount);
|
|
55510
|
+
return Math.min(Math.max(rows, PARQUET_MIN_ROW_GROUP_ROWS), PARQUET_MAX_ROW_GROUP_ROWS);
|
|
55511
|
+
};
|
|
55512
|
+
/** A positional line read onto the column names by position — the file holds the visible columns only. */
|
|
55513
|
+
this._positionalValues = (line, columns) => {
|
|
55514
|
+
const parts = DatasetCodec_default.decodeRow(line);
|
|
55515
|
+
const values = {};
|
|
55516
|
+
for (const [index, column] of columns.entries())
|
|
55517
|
+
values[column.name] = parts[index];
|
|
55518
|
+
return values;
|
|
55519
|
+
};
|
|
55520
|
+
/**
|
|
55521
|
+
* One value as its column holds it. A missing value is a null in the column rather than the type's
|
|
55522
|
+
* zero, so a reader can still tell "no value" from a real `0`, `false` or empty string.
|
|
55523
|
+
*
|
|
55524
|
+
* Missing is null/undefined, plus — for every type but `string` — the empty string, matching
|
|
55525
|
+
* `TypeCaster.castDimension`: that is how a NULL arrives from every source that stages through
|
|
55526
|
+
* text. An empty string IS a value for a string column and is kept.
|
|
55527
|
+
*/
|
|
55528
|
+
this._toParquetValue = (value, column, consumerName) => {
|
|
55529
|
+
if (value === null || value === void 0) return null;
|
|
55530
|
+
if (value === "" && column.type !== "STRING") return null;
|
|
55531
|
+
switch (column.type) {
|
|
55532
|
+
case "BOOLEAN":
|
|
55533
|
+
return typeof value === "boolean" ? value : TRUE_VALUES2.test(String(value).trim());
|
|
55534
|
+
case "DOUBLE":
|
|
55535
|
+
return typeof value === "number" ? value : Number(value);
|
|
55536
|
+
case "TIMESTAMP":
|
|
55537
|
+
return this._toDate(value, column.name, consumerName);
|
|
55538
|
+
default:
|
|
55539
|
+
return typeof value === "string" ? value : String(value);
|
|
55540
|
+
}
|
|
55541
|
+
};
|
|
55542
|
+
/**
|
|
55543
|
+
* A `datetime` column's value as a Date. An unparseable one FAILS the run, naming the field and
|
|
55544
|
+
* the value: a consumer whose transformation reformats a date into "DDMMYYYY" under a `datetime`
|
|
55545
|
+
* dimension is a configuration error, and a timestamp column silently full of 1970 hides it until
|
|
55546
|
+
* someone queries the lake.
|
|
55547
|
+
*/
|
|
55548
|
+
this._toDate = (value, name, consumerName) => {
|
|
55549
|
+
const date2 = value instanceof Date ? value : new Date(value);
|
|
55550
|
+
if (isNaN(date2.getTime()))
|
|
55551
|
+
throw new Error(`Consumer "${consumerName}" cannot export field "${name}" as a parquet TIMESTAMP: "${String(value)}" is not a date. The field declares a "datetime" dimension, so either the value or that declaration is wrong.`);
|
|
55552
|
+
return date2;
|
|
55553
|
+
};
|
|
55554
|
+
}
|
|
55555
|
+
/**
|
|
55556
|
+
* The dataset's records, one at a time, coerced to what each column holds. An async generator, so
|
|
55557
|
+
* the writer pulls a row group's worth and no more — the file is never in memory as a whole.
|
|
55558
|
+
*
|
|
55559
|
+
* A parquet consumer stages JSON (see `DatasetSchema.encodingFor`), so the values arrive already
|
|
55560
|
+
* typed. The positional branch covers a dataset staged the other way, reading the row WITHOUT
|
|
55561
|
+
* casting since the cast is what loses a boolean (`Boolean('false')` is `true`).
|
|
55562
|
+
*/
|
|
55563
|
+
async *_readRecords(request, columns) {
|
|
55564
|
+
const readStream = fs24.createReadStream(request.sourcePath);
|
|
55565
|
+
try {
|
|
55566
|
+
for await (const line of readLines(readStream)) {
|
|
55567
|
+
const values = request.dsSchema.encoding === "CSV" ? this._positionalValues(line, columns) : JSON.parse(line);
|
|
55568
|
+
const row = {};
|
|
55569
|
+
for (const column of columns)
|
|
55570
|
+
row[column.name] = this._toParquetValue(values[column.name], column, request.consumerName);
|
|
55571
|
+
yield row;
|
|
55572
|
+
}
|
|
55573
|
+
} finally {
|
|
55574
|
+
readStream.destroy();
|
|
55575
|
+
}
|
|
55576
|
+
}
|
|
55577
|
+
};
|
|
55578
|
+
ParquetExporter = new ParquetExporterClass();
|
|
55579
|
+
ParquetExporter_default = ParquetExporter;
|
|
55580
|
+
}
|
|
55581
|
+
});
|
|
55582
|
+
|
|
54989
55583
|
// ../../packages/engines/src/producer/PushdownEngine.ts
|
|
54990
55584
|
var MISSING_BY_TYPE, PushdownEngineClass, PushdownEngine, PushdownEngine_default;
|
|
54991
55585
|
var init_PushdownEngine = __esm({
|
|
@@ -55818,6 +56412,7 @@ var init_src10 = __esm({
|
|
|
55818
56412
|
init_RunOverlay();
|
|
55819
56413
|
init_RunRegistry();
|
|
55820
56414
|
init_LineParser();
|
|
56415
|
+
init_ParquetExporter();
|
|
55821
56416
|
init_DiscoverMerger();
|
|
55822
56417
|
init_ProducerEngine();
|
|
55823
56418
|
init_ProducerManager();
|
|
@@ -55833,6 +56428,216 @@ var init_src10 = __esm({
|
|
|
55833
56428
|
}
|
|
55834
56429
|
});
|
|
55835
56430
|
|
|
56431
|
+
// ../../packages/executors/src/DatasetSchema.ts
|
|
56432
|
+
var DatasetSchemaClass, DatasetSchema, DatasetSchema_default;
|
|
56433
|
+
var init_DatasetSchema = __esm({
|
|
56434
|
+
"../../packages/executors/src/DatasetSchema.ts"() {
|
|
56435
|
+
init_src();
|
|
56436
|
+
init_src10();
|
|
56437
|
+
DatasetSchemaClass = class {
|
|
56438
|
+
constructor() {
|
|
56439
|
+
/**
|
|
56440
|
+
* How a consumer's records are staged on the way to the destination. A consumer declares exactly
|
|
56441
|
+
* one output (`maxItems: 1` in consumer-schema.json), so `outputs[0]` is THE output.
|
|
56442
|
+
*/
|
|
56443
|
+
this.encodingFor = (consumer) => {
|
|
56444
|
+
Affirm_default(consumer, "Invalid consumer");
|
|
56445
|
+
Affirm_default.hasItems(consumer.outputs, `Consumer "${consumer.name}" declares no output`);
|
|
56446
|
+
switch (consumer.outputs[0].format) {
|
|
56447
|
+
case "JSON":
|
|
56448
|
+
case "API":
|
|
56449
|
+
return "JSON";
|
|
56450
|
+
case "PARQUET":
|
|
56451
|
+
return "JSON";
|
|
56452
|
+
case "TXT":
|
|
56453
|
+
return "CSV";
|
|
56454
|
+
case "FIXED":
|
|
56455
|
+
return "CSV";
|
|
56456
|
+
case "CSV":
|
|
56457
|
+
return "CSV";
|
|
56458
|
+
default:
|
|
56459
|
+
throw new Error(`Export format ${consumer.outputs[0].format} not implemented yet.`);
|
|
56460
|
+
}
|
|
56461
|
+
};
|
|
56462
|
+
/**
|
|
56463
|
+
* The schema the run's workers write with: one column per VISIBLE consumer field, in field
|
|
56464
|
+
* order. Hidden fields are not on the line at all.
|
|
56465
|
+
*/
|
|
56466
|
+
this.compile = (consumer, fields) => {
|
|
56467
|
+
Affirm_default(consumer, "Invalid consumer");
|
|
56468
|
+
Affirm_default(fields, "Invalid consumer fields");
|
|
56469
|
+
const columns = fields.filter((x2) => !x2.cField.hidden).map(this._column);
|
|
56470
|
+
Affirm_default.hasItems(columns, `Consumer "${consumer.name}" has no visible field to write`);
|
|
56471
|
+
return {
|
|
56472
|
+
encoding: this.encodingFor(consumer),
|
|
56473
|
+
columns,
|
|
56474
|
+
rows: 0,
|
|
56475
|
+
writtenBy: "worker"
|
|
56476
|
+
};
|
|
56477
|
+
};
|
|
56478
|
+
/**
|
|
56479
|
+
* The same shape, as the named stage left it. Returned by every pass that rewrites the dataset
|
|
56480
|
+
* without changing its columns, so `rows` always describes the file as it is NOW.
|
|
56481
|
+
*/
|
|
56482
|
+
this.rewritten = (dsSchema, writtenBy, rows) => {
|
|
56483
|
+
Affirm_default(dsSchema, "Invalid dataset schema");
|
|
56484
|
+
Affirm_default(rows >= 0, `Invalid row count ${rows} for a dataset written by "${writtenBy}"`);
|
|
56485
|
+
return { ...dsSchema, writtenBy, rows };
|
|
56486
|
+
};
|
|
56487
|
+
/**
|
|
56488
|
+
* The shape a `pivot` leaves behind: the row keys it grouped on, then one column per pivot value.
|
|
56489
|
+
* A pivoted column has no producer dimension behind it - it is an aggregate this pass invented -
|
|
56490
|
+
* so it declares `source: 'pivot'` and a `number` type.
|
|
56491
|
+
*/
|
|
56492
|
+
this.forPivot = (dsSchema, rowKeys, pivotColumns, rows) => {
|
|
56493
|
+
Affirm_default(dsSchema, "Invalid dataset schema");
|
|
56494
|
+
Affirm_default.hasItems(rowKeys, "A pivot declares no row key");
|
|
56495
|
+
const keyColumns = rowKeys.map((key) => {
|
|
56496
|
+
const existing = dsSchema.columns.find((column) => column.name === key);
|
|
56497
|
+
Affirm_default(existing, `The pivot row key "${key}" is not a column of the dataset being pivoted`);
|
|
56498
|
+
return existing;
|
|
56499
|
+
});
|
|
56500
|
+
return {
|
|
56501
|
+
encoding: dsSchema.encoding,
|
|
56502
|
+
columns: [...keyColumns, ...pivotColumns.map((name) => ({ name, type: "number", source: "pivot" }))],
|
|
56503
|
+
rows,
|
|
56504
|
+
writtenBy: "pivot"
|
|
56505
|
+
};
|
|
56506
|
+
};
|
|
56507
|
+
/** The column names on a line, in line order - the header of a positional export. */
|
|
56508
|
+
this.names = (dsSchema) => {
|
|
56509
|
+
Affirm_default(dsSchema, "Invalid dataset schema");
|
|
56510
|
+
return dsSchema.columns.map((column) => column.name);
|
|
56511
|
+
};
|
|
56512
|
+
/**
|
|
56513
|
+
* One column, typed from the dimension behind it. `type`, `format` and `nullable` are the only
|
|
56514
|
+
* three properties a cast reads, so carrying those lets a reader cast a staged value back
|
|
56515
|
+
* without going near the consumer config.
|
|
56516
|
+
*/
|
|
56517
|
+
this._column = (field) => {
|
|
56518
|
+
if (ConsumerManager_default.isLineCountField(field.cField))
|
|
56519
|
+
return { name: field.finalKey, type: "number", source: "lineCount" };
|
|
56520
|
+
const column = {
|
|
56521
|
+
name: field.finalKey,
|
|
56522
|
+
type: field.dimension?.type ?? "string"
|
|
56523
|
+
};
|
|
56524
|
+
if (field.dimension?.format) column.format = field.dimension.format;
|
|
56525
|
+
if (field.dimension?.nullable === false) column.nullable = false;
|
|
56526
|
+
return column;
|
|
56527
|
+
};
|
|
56528
|
+
}
|
|
56529
|
+
};
|
|
56530
|
+
DatasetSchema = new DatasetSchemaClass();
|
|
56531
|
+
DatasetSchema_default = DatasetSchema;
|
|
56532
|
+
}
|
|
56533
|
+
});
|
|
56534
|
+
|
|
56535
|
+
// ../../packages/executors/src/FixedBlockOrder.ts
|
|
56536
|
+
var fs25, FixedBlockOrderClass, FixedBlockOrder, FixedBlockOrder_default;
|
|
56537
|
+
var init_FixedBlockOrder = __esm({
|
|
56538
|
+
"../../packages/executors/src/FixedBlockOrder.ts"() {
|
|
56539
|
+
fs25 = __toESM(require("fs"));
|
|
56540
|
+
init_src();
|
|
56541
|
+
init_src5();
|
|
56542
|
+
init_src2();
|
|
56543
|
+
FixedBlockOrderClass = class {
|
|
56544
|
+
constructor() {
|
|
56545
|
+
/** The file one block's records are collected in. Named by the value's place in `order`, not by
|
|
56546
|
+
* the value itself: a discriminator value is data and may hold anything a path may not. */
|
|
56547
|
+
this.blockPath = (sourcePath, position) => `${sourcePath}.block_${position}`;
|
|
56548
|
+
/**
|
|
56549
|
+
* Routes the staged dataset into one file per layout and concatenates them in `layouts.order`,
|
|
56550
|
+
* returning the path of the block-ordered dataset the export then transforms.
|
|
56551
|
+
*/
|
|
56552
|
+
this.reorder = async (request) => {
|
|
56553
|
+
Affirm_default(request, "Invalid block order request");
|
|
56554
|
+
Affirm_default(request.sourcePath, "Missing dataset to order into blocks");
|
|
56555
|
+
Affirm_default(request.layouts, "Invalid fixed-width layouts");
|
|
56556
|
+
Affirm_default.hasItems(request.layouts.order, "The fixed-width layouts declare no block order");
|
|
56557
|
+
const { sourcePath, layouts, consumerName } = request;
|
|
56558
|
+
const orderedPath = `${sourcePath}.blocks`;
|
|
56559
|
+
const counts = await this._route(request);
|
|
56560
|
+
await fs25.promises.unlink(sourcePath);
|
|
56561
|
+
await this._concat(sourcePath, layouts, orderedPath);
|
|
56562
|
+
Logger_default.info(`Consumer "${consumerName}" writes its FIXED record blocks as ${layouts.order.map((value, position) => `${value} x${counts[position]}`).join(", ")}`);
|
|
56563
|
+
return orderedPath;
|
|
56564
|
+
};
|
|
56565
|
+
/**
|
|
56566
|
+
* The routing pass: every line read once, appended to the file of the block its discriminator
|
|
56567
|
+
* names. One stream per layout, all open at once — `MAX_FIXED_LAYOUTS` is what bounds them, so
|
|
56568
|
+
* there is no eviction to do and a block's file is only ever written by its own stream.
|
|
56569
|
+
*
|
|
56570
|
+
* Returns the records each block took, in `order`, which is what the log line reports.
|
|
56571
|
+
*/
|
|
56572
|
+
this._route = async (request) => {
|
|
56573
|
+
const { sourcePath, layouts } = request;
|
|
56574
|
+
const streams = layouts.order.map((_3, position) => fs25.createWriteStream(this.blockPath(sourcePath, position)));
|
|
56575
|
+
const positions = new Map(layouts.order.map((value, position) => [value, position]));
|
|
56576
|
+
const counts = layouts.order.map(() => 0);
|
|
56577
|
+
const readStream = fs25.createReadStream(sourcePath);
|
|
56578
|
+
try {
|
|
56579
|
+
for await (const line of readLines(readStream)) {
|
|
56580
|
+
const position = this._positionOf(line, layouts, positions);
|
|
56581
|
+
counts[position]++;
|
|
56582
|
+
const stream = streams[position];
|
|
56583
|
+
if (!stream.write(line + "\n"))
|
|
56584
|
+
await new Promise((resolve) => stream.once("drain", resolve));
|
|
56585
|
+
}
|
|
56586
|
+
} finally {
|
|
56587
|
+
for (const stream of streams)
|
|
56588
|
+
await this._closeStream(stream);
|
|
56589
|
+
readStream.destroy();
|
|
56590
|
+
}
|
|
56591
|
+
return counts;
|
|
56592
|
+
};
|
|
56593
|
+
/** The block a staged line belongs to. A record no layout covers stops the run; see `planFor`. */
|
|
56594
|
+
this._positionOf = (line, layouts, positions) => {
|
|
56595
|
+
const value = DatasetCodec_default.decodeRow(line)[layouts.index];
|
|
56596
|
+
const position = value === null || value === void 0 ? void 0 : positions.get(String(value));
|
|
56597
|
+
Affirm_default(
|
|
56598
|
+
position !== void 0,
|
|
56599
|
+
`No FIXED layout for the value ${value === null || value === void 0 ? "null" : `"${value}"`} of the discriminator field "${layouts.discriminator}". The layouts cover ${layouts.order.map((x2) => `"${x2}"`).join(", ")}.`
|
|
56600
|
+
);
|
|
56601
|
+
return position;
|
|
56602
|
+
};
|
|
56603
|
+
/** The blocks appended into one dataset, in `order`, each file removed as it is consumed. */
|
|
56604
|
+
this._concat = async (sourcePath, layouts, orderedPath) => {
|
|
56605
|
+
const writer = fs25.createWriteStream(orderedPath);
|
|
56606
|
+
try {
|
|
56607
|
+
for (let position = 0; position < layouts.order.length; position++) {
|
|
56608
|
+
const blockPath = this.blockPath(sourcePath, position);
|
|
56609
|
+
await this._append(blockPath, writer);
|
|
56610
|
+
await fs25.promises.unlink(blockPath);
|
|
56611
|
+
}
|
|
56612
|
+
} finally {
|
|
56613
|
+
await this._closeStream(writer);
|
|
56614
|
+
}
|
|
56615
|
+
};
|
|
56616
|
+
/** One block's bytes onto the end of the ordered dataset. Piped, so a block is never held in memory. */
|
|
56617
|
+
this._append = (blockPath, writer) => {
|
|
56618
|
+
return new Promise((resolve, reject) => {
|
|
56619
|
+
const reader = fs25.createReadStream(blockPath);
|
|
56620
|
+
reader.on("error", reject);
|
|
56621
|
+
reader.on("end", resolve);
|
|
56622
|
+
reader.pipe(writer, { end: false });
|
|
56623
|
+
});
|
|
56624
|
+
};
|
|
56625
|
+
/** Flushes and releases a stream's descriptor. Awaited: the read that follows must not race the flush. */
|
|
56626
|
+
this._closeStream = async (stream) => {
|
|
56627
|
+
if (!stream) return;
|
|
56628
|
+
stream.end();
|
|
56629
|
+
await new Promise((resolve, reject) => {
|
|
56630
|
+
stream.on("finish", resolve);
|
|
56631
|
+
stream.on("error", reject);
|
|
56632
|
+
});
|
|
56633
|
+
};
|
|
56634
|
+
}
|
|
56635
|
+
};
|
|
56636
|
+
FixedBlockOrder = new FixedBlockOrderClass();
|
|
56637
|
+
FixedBlockOrder_default = FixedBlockOrder;
|
|
56638
|
+
}
|
|
56639
|
+
});
|
|
56640
|
+
|
|
55836
56641
|
// ../../packages/executors/src/OutputExecutor.ts
|
|
55837
56642
|
var OutputExecutorClass, OutputExecutor, OutputExecutor_default;
|
|
55838
56643
|
var init_OutputExecutor = __esm({
|
|
@@ -55841,35 +56646,25 @@ var init_OutputExecutor = __esm({
|
|
|
55841
56646
|
init_src();
|
|
55842
56647
|
init_src8();
|
|
55843
56648
|
init_src4();
|
|
56649
|
+
init_src10();
|
|
55844
56650
|
init_src5();
|
|
55845
56651
|
init_src2();
|
|
56652
|
+
init_DatasetSchema();
|
|
56653
|
+
init_FixedBlockOrder();
|
|
55846
56654
|
OutputExecutorClass = class {
|
|
55847
56655
|
constructor() {
|
|
55848
|
-
this._getInternalRecordFormat = (consumer) => {
|
|
55849
|
-
const output = consumer.outputs[0];
|
|
55850
|
-
switch (output.format) {
|
|
55851
|
-
case "API":
|
|
55852
|
-
return "JSON";
|
|
55853
|
-
case "TXT":
|
|
55854
|
-
return "CSV";
|
|
55855
|
-
default:
|
|
55856
|
-
return output.format;
|
|
55857
|
-
}
|
|
55858
|
-
};
|
|
55859
56656
|
/**
|
|
55860
|
-
* The
|
|
55861
|
-
*
|
|
55862
|
-
* all three for every record, and `toCSV` allocated a filtered field array on top of that.
|
|
56657
|
+
* The write side of a staged dataset's schema: what to write, in what order, in which encoding.
|
|
56658
|
+
* Derived from the schema, so the writer and every reader share one column list.
|
|
55863
56659
|
*/
|
|
55864
|
-
this.compileOutput = (consumer,
|
|
56660
|
+
this.compileOutput = (consumer, dsSchema) => {
|
|
55865
56661
|
Affirm_default(consumer, "Invalid consumer");
|
|
55866
|
-
Affirm_default(
|
|
56662
|
+
Affirm_default(dsSchema, "Invalid dataset schema");
|
|
55867
56663
|
return {
|
|
55868
|
-
format:
|
|
56664
|
+
format: dsSchema.encoding,
|
|
55869
56665
|
delimiter: ",",
|
|
55870
|
-
visibleKeys:
|
|
55871
|
-
|
|
55872
|
-
omitNull: consumer.outputs.some((x2) => x2.omitNull === true)
|
|
56666
|
+
visibleKeys: DatasetSchema_default.names(dsSchema),
|
|
56667
|
+
omitNull: consumer.outputs[0].omitNull === true
|
|
55873
56668
|
};
|
|
55874
56669
|
};
|
|
55875
56670
|
this.outputRecord = (record, plan) => {
|
|
@@ -55882,26 +56677,38 @@ var init_OutputExecutor = __esm({
|
|
|
55882
56677
|
throw new Error(`Export format ${plan.format} not implemented yet.`);
|
|
55883
56678
|
}
|
|
55884
56679
|
};
|
|
56680
|
+
/**
|
|
56681
|
+
* One positional line of the internal format. `DatasetCodec`, not `CSVParser`: a staged line has
|
|
56682
|
+
* to carry a null that is not an empty string and must never hold a newline.
|
|
56683
|
+
*/
|
|
55885
56684
|
this.toCSV = (record, plan) => {
|
|
55886
56685
|
const { visibleKeys, delimiter } = plan;
|
|
55887
56686
|
let row = "";
|
|
55888
56687
|
for (let index = 0; index < visibleKeys.length; index++) {
|
|
55889
56688
|
if (index > 0)
|
|
55890
56689
|
row += delimiter;
|
|
55891
|
-
row +=
|
|
56690
|
+
row += DatasetCodec_default.encodeField(record[visibleKeys[index]]);
|
|
55892
56691
|
}
|
|
55893
56692
|
return row;
|
|
55894
56693
|
};
|
|
56694
|
+
/**
|
|
56695
|
+
* One JSON object per line, keyed in the CONSUMER's field order rather than the record's own.
|
|
56696
|
+
*
|
|
56697
|
+
* A record arrives keyed in the order its producer's dimensions were read, so two producers of
|
|
56698
|
+
* one consumer wrote the same record as two different lines — and `distinct` and
|
|
56699
|
+
* `no_duplicates`, which compare the line, saw two rows. Building from `visibleKeys` gives every
|
|
56700
|
+
* writer one order, and drops the hidden fields without deleting them out of the caller's record.
|
|
56701
|
+
*/
|
|
55895
56702
|
this.toJSON = (record, plan) => {
|
|
55896
|
-
|
|
55897
|
-
|
|
55898
|
-
|
|
55899
|
-
|
|
55900
|
-
|
|
55901
|
-
|
|
55902
|
-
|
|
56703
|
+
const { visibleKeys, omitNull } = plan;
|
|
56704
|
+
const output = {};
|
|
56705
|
+
for (const key of visibleKeys) {
|
|
56706
|
+
const value = record[key];
|
|
56707
|
+
if (omitNull && (value === null || value === void 0))
|
|
56708
|
+
continue;
|
|
56709
|
+
output[key] = value === void 0 ? null : value;
|
|
55903
56710
|
}
|
|
55904
|
-
return JSON.stringify(
|
|
56711
|
+
return JSON.stringify(output);
|
|
55905
56712
|
};
|
|
55906
56713
|
/**
|
|
55907
56714
|
* The exported name of one split file: the destination name with the file's label before the
|
|
@@ -55914,66 +56721,141 @@ var init_OutputExecutor = __esm({
|
|
|
55914
56721
|
return `${pathName.substring(0, dot)}_${label}${pathName.substring(dot)}`;
|
|
55915
56722
|
};
|
|
55916
56723
|
/**
|
|
55917
|
-
* Writes the run's result to
|
|
56724
|
+
* Writes the run's result to the consumer's destination, one call per file the split pass
|
|
55918
56725
|
* produced. The files are passed in rather than read back out of the scope folder: the label a
|
|
55919
56726
|
* file carries is what names it at the destination, and with no split configured that list is
|
|
55920
56727
|
* the merged dataset alone, labelled "0" — the suffix an unsplit export has always had.
|
|
56728
|
+
*
|
|
56729
|
+
* One output per consumer (`maxItems: 1` in consumer-schema.json). This used to loop, but only
|
|
56730
|
+
* ever shipped the first entry, and could not have done more: `move` unlinks what it ships.
|
|
55921
56731
|
*/
|
|
55922
|
-
this.exportResult = async (consumer,
|
|
56732
|
+
this.exportResult = async (consumer, dsSchema, splitFiles) => {
|
|
55923
56733
|
Affirm_default.hasItems(splitFiles, `Consumer "${consumer.name}" has no output file to export`);
|
|
55924
|
-
|
|
56734
|
+
Affirm_default.hasItems(consumer.outputs, `Consumer "${consumer.name}" declares no output to export to`);
|
|
56735
|
+
Affirm_default(dsSchema, `Consumer "${consumer.name}" has no dataset schema to export against`);
|
|
56736
|
+
const output = consumer.outputs[0];
|
|
56737
|
+
const internalFormat = dsSchema.encoding;
|
|
56738
|
+
const destination = Environment_default.getSource(output.exportDestination);
|
|
56739
|
+
Affirm_default(destination, `The export destination "${output.exportDestination}" of consumer "${consumer.name}" is not a source of this project`);
|
|
56740
|
+
const driver = await DriverFactory_default.instantiateDestination(destination);
|
|
56741
|
+
const destinationName = this._composeFileName(consumer, output, this._getExtension(output));
|
|
56742
|
+
Logger_default.info(`Exporting consumer "${consumer.name}" to "${output.exportDestination}" as ${output.format} (${destinationName}, ${splitFiles.length} file(s))`);
|
|
56743
|
+
const header = this._composeHeader(output, dsSchema);
|
|
56744
|
+
const fixedLayouts = output.format === "FIXED" && output.layouts ? FixedWidthWriter_default.compileLayouts(dsSchema, this._fixedPositions(consumer), output, consumer.name) : null;
|
|
56745
|
+
const fixedLine = output.format === "FIXED" ? this._fixedLineWriter(consumer, dsSchema, output, fixedLayouts) : null;
|
|
55925
56746
|
const results = [];
|
|
55926
|
-
for (const
|
|
55927
|
-
const
|
|
55928
|
-
|
|
55929
|
-
|
|
55930
|
-
|
|
55931
|
-
|
|
55932
|
-
|
|
55933
|
-
|
|
55934
|
-
|
|
55935
|
-
results.push(
|
|
55936
|
-
|
|
55937
|
-
|
|
55938
|
-
|
|
55939
|
-
|
|
55940
|
-
|
|
55941
|
-
|
|
55942
|
-
|
|
55943
|
-
|
|
55944
|
-
|
|
55945
|
-
|
|
55946
|
-
|
|
55947
|
-
|
|
55948
|
-
|
|
55949
|
-
await driver.transformAndMove(
|
|
55950
|
-
startingPath,
|
|
55951
|
-
(line) => CSVParser_default.stringifyRow(CSVParser_default.parseRow(line, ","), delimiter),
|
|
55952
|
-
destinationPath,
|
|
55953
|
-
header
|
|
55954
|
-
)
|
|
55955
|
-
);
|
|
55956
|
-
break;
|
|
55957
|
-
}
|
|
55958
|
-
case "API":
|
|
55959
|
-
case "JSON":
|
|
55960
|
-
results.push(
|
|
55961
|
-
await driver.transformAndMove(startingPath, (line) => {
|
|
55962
|
-
const parts = CSVParser_default.parseRow(line, ",");
|
|
55963
|
-
const value = {};
|
|
55964
|
-
for (const [index, field] of fields.entries())
|
|
55965
|
-
value[field.finalKey] = parts[index];
|
|
55966
|
-
return JSON.stringify(value);
|
|
55967
|
-
}, destinationPath)
|
|
55968
|
-
);
|
|
55969
|
-
break;
|
|
55970
|
-
default:
|
|
55971
|
-
throw new Error(`Export result to format ${output.format} not implemented yet.`);
|
|
55972
|
-
}
|
|
56747
|
+
for (const { path: startingPath, label } of splitFiles) {
|
|
56748
|
+
const destinationPath = this.getCompletedPath(destinationName, label);
|
|
56749
|
+
if (output.format === internalFormat || output.format === "API") {
|
|
56750
|
+
results.push(await driver.move(startingPath, destinationPath, header));
|
|
56751
|
+
continue;
|
|
56752
|
+
}
|
|
56753
|
+
switch (output.format) {
|
|
56754
|
+
case "TXT": {
|
|
56755
|
+
const delimiter = this._getDelimiter(output);
|
|
56756
|
+
results.push(
|
|
56757
|
+
await driver.transformAndMove(
|
|
56758
|
+
startingPath,
|
|
56759
|
+
(line) => DatasetCodec_default.redelimit(line, delimiter),
|
|
56760
|
+
destinationPath,
|
|
56761
|
+
header
|
|
56762
|
+
)
|
|
56763
|
+
);
|
|
56764
|
+
break;
|
|
56765
|
+
}
|
|
56766
|
+
case "FIXED": {
|
|
56767
|
+
const fixedPath = fixedLayouts ? await FixedBlockOrder_default.reorder({ sourcePath: startingPath, layouts: fixedLayouts, consumerName: consumer.name }) : startingPath;
|
|
56768
|
+
results.push(await driver.transformAndMove(fixedPath, fixedLine, destinationPath));
|
|
56769
|
+
break;
|
|
55973
56770
|
}
|
|
56771
|
+
case "PARQUET":
|
|
56772
|
+
results.push(await this._exportParquet({
|
|
56773
|
+
sourcePath: startingPath,
|
|
56774
|
+
destinationPath,
|
|
56775
|
+
dsSchema,
|
|
56776
|
+
consumerName: consumer.name,
|
|
56777
|
+
driver
|
|
56778
|
+
}));
|
|
56779
|
+
break;
|
|
56780
|
+
default:
|
|
56781
|
+
throw new Error(`Export result to format ${output.format} not implemented yet.`);
|
|
55974
56782
|
}
|
|
55975
|
-
return this._unifingResult(results);
|
|
55976
56783
|
}
|
|
56784
|
+
return this._unifingResult(results);
|
|
56785
|
+
};
|
|
56786
|
+
/**
|
|
56787
|
+
* A consumer's FIXED output resolved without running it: the layouts the export would compile,
|
|
56788
|
+
* and every problem with them. `compile` prints what it resolves and fails on what it reports,
|
|
56789
|
+
* and a run resolves it before reading a producer — so a width that does not add up is caught
|
|
56790
|
+
* where it was written rather than after the whole dataset has been built.
|
|
56791
|
+
*
|
|
56792
|
+
* Null for a consumer that writes anything else: there is no layout to resolve.
|
|
56793
|
+
*/
|
|
56794
|
+
this.reviewFixedOutput = (consumer, dsSchema) => {
|
|
56795
|
+
Affirm_default(consumer, "Invalid consumer");
|
|
56796
|
+
const output = consumer.outputs?.[0];
|
|
56797
|
+
if (output?.format !== "FIXED") return null;
|
|
56798
|
+
const schema = dsSchema ?? DatasetSchema_default.compile(consumer, ConsumerManager_default.getExpandedFields(consumer));
|
|
56799
|
+
return FixedWidthWriter_default.review(schema, this._fixedPositions(consumer), output, consumer.name);
|
|
56800
|
+
};
|
|
56801
|
+
/**
|
|
56802
|
+
* The line function a FIXED export hands its destination driver: one staged record in, the
|
|
56803
|
+
* positional line it becomes out. The layout is resolved here, once, and closed over — so the
|
|
56804
|
+
* per-record cost is a decode of the staged line and a pad per field, with no config lookup.
|
|
56805
|
+
*
|
|
56806
|
+
* With `layouts` declared the record's own discriminator picks its plan, which is the only
|
|
56807
|
+
* per-record branch on the path; the records are already grouped into their blocks by then.
|
|
56808
|
+
*
|
|
56809
|
+
* No header is passed alongside it: a positional file has no room for one, and the field names
|
|
56810
|
+
* would not sit on the columns they describe.
|
|
56811
|
+
*/
|
|
56812
|
+
this._fixedLineWriter = (consumer, dsSchema, output, layouts) => {
|
|
56813
|
+
const suffix = FixedWidthWriter_default.lineSuffix(output);
|
|
56814
|
+
if (layouts) {
|
|
56815
|
+
for (const value of layouts.order)
|
|
56816
|
+
Logger_default.info(`Consumer "${consumer.name}" writes its FIXED "${value}" record as ${layouts.plans.get(value).recordLength} characters over ${layouts.plans.get(value).fields.length} field(s)`);
|
|
56817
|
+
return (line) => {
|
|
56818
|
+
const values = DatasetCodec_default.decodeRow(line);
|
|
56819
|
+
return FixedWidthWriter_default.composeLine(values, FixedWidthWriter_default.planFor(values, layouts)) + suffix;
|
|
56820
|
+
};
|
|
56821
|
+
}
|
|
56822
|
+
const plan = FixedWidthWriter_default.compilePlan(dsSchema.columns, this._fixedPositions(consumer), output, `the FIXED output of consumer "${consumer.name}"`);
|
|
56823
|
+
Logger_default.info(`Consumer "${consumer.name}" writes a FIXED record of ${plan.recordLength} characters over ${plan.fields.length} field(s)`);
|
|
56824
|
+
return (line) => FixedWidthWriter_default.composeLine(DatasetCodec_default.decodeRow(line), plan) + suffix;
|
|
56825
|
+
};
|
|
56826
|
+
/**
|
|
56827
|
+
* Every visible field's `position` block, keyed by the name its column carries on the staged
|
|
56828
|
+
* line. Read from the EXPANDED fields rather than the consumer's own, so a glob key brings the
|
|
56829
|
+
* `position` it was declared with to each of the columns it stands for, and a field the dataset
|
|
56830
|
+
* has no `position` for is left absent for `compilePlan` to name.
|
|
56831
|
+
*/
|
|
56832
|
+
this._fixedPositions = (consumer) => {
|
|
56833
|
+
const positions = {};
|
|
56834
|
+
for (const field of ConsumerManager_default.getExpandedFields(consumer)) {
|
|
56835
|
+
if (field.cField.hidden) continue;
|
|
56836
|
+
positions[field.finalKey] = field.cField.position;
|
|
56837
|
+
}
|
|
56838
|
+
return positions;
|
|
56839
|
+
};
|
|
56840
|
+
/**
|
|
56841
|
+
* Ships one split file as parquet. A parquet file is a columnar container whose footer can only
|
|
56842
|
+
* be written once the last row group is encoded, so there is no per-line mapping to hand a
|
|
56843
|
+
* driver: the file is built in the scope folder beside the dataset it is read from, then handed
|
|
56844
|
+
* over as bytes with NO header — a parquet file carries its own schema, and `move` is
|
|
56845
|
+
* byte-faithful in all three destination drivers.
|
|
56846
|
+
*
|
|
56847
|
+
* `move` unlinks what it shipped, so a successful export leaves no staged file behind. A failed
|
|
56848
|
+
* one leaves it in the scope folder, which the run's cleanup removes with everything else.
|
|
56849
|
+
*/
|
|
56850
|
+
this._exportParquet = async (plan) => {
|
|
56851
|
+
const parquetPath = `${plan.sourcePath}.parquet`;
|
|
56852
|
+
await ParquetExporter_default.exportToParquet({
|
|
56853
|
+
sourcePath: plan.sourcePath,
|
|
56854
|
+
targetPath: parquetPath,
|
|
56855
|
+
dsSchema: plan.dsSchema,
|
|
56856
|
+
consumerName: plan.consumerName
|
|
56857
|
+
});
|
|
56858
|
+
return plan.driver.move(parquetPath, plan.destinationPath);
|
|
55977
56859
|
};
|
|
55978
56860
|
/**
|
|
55979
56861
|
* Folds the per-file upload results into the one the run reports.
|
|
@@ -55992,24 +56874,32 @@ var init_OutputExecutor = __esm({
|
|
|
55992
56874
|
/**
|
|
55993
56875
|
* The header line of a positional export: the visible field names, in column order, quoted and
|
|
55994
56876
|
* delimited exactly as the rows under them — so the header parses with the same reader the data
|
|
55995
|
-
* does. Null for a JSON/API output, where a record already carries its own field names,
|
|
55996
|
-
*
|
|
56877
|
+
* does. Null for a JSON/API output, where a record already carries its own field names, for a
|
|
56878
|
+
* FIXED one, where a name does not fit the column it describes and the receiver reads by
|
|
56879
|
+
* position anyway, and for an output that turns the header off.
|
|
55997
56880
|
*
|
|
55998
|
-
* The names
|
|
55999
|
-
* of the file has nothing else to match a column against.
|
|
56881
|
+
* The names come from the dataset's own schema, so a `pivot` is described by the header.
|
|
56000
56882
|
*/
|
|
56001
|
-
this._composeHeader = (output,
|
|
56883
|
+
this._composeHeader = (output, dsSchema) => {
|
|
56002
56884
|
if (output.header === false) return null;
|
|
56003
56885
|
if (output.format !== "CSV" && output.format !== "TXT") return null;
|
|
56004
|
-
|
|
56005
|
-
return CSVParser_default.stringifyRow(names, this._getDelimiter(output));
|
|
56886
|
+
return CSVParser_default.stringifyRow(DatasetSchema_default.names(dsSchema), this._getDelimiter(output));
|
|
56006
56887
|
};
|
|
56007
56888
|
/** The delimiter the exported file is written with. Only TXT configures it; CSV is a comma. */
|
|
56008
56889
|
this._getDelimiter = (output) => {
|
|
56009
56890
|
return output.format === "TXT" ? output.delimiter ?? " " : ",";
|
|
56010
56891
|
};
|
|
56011
56892
|
this._getExtension = (output) => {
|
|
56012
|
-
|
|
56893
|
+
switch (output.format) {
|
|
56894
|
+
case "CSV":
|
|
56895
|
+
return "csv";
|
|
56896
|
+
case "JSON":
|
|
56897
|
+
return "jsonl";
|
|
56898
|
+
case "PARQUET":
|
|
56899
|
+
return "parquet";
|
|
56900
|
+
default:
|
|
56901
|
+
return "txt";
|
|
56902
|
+
}
|
|
56013
56903
|
};
|
|
56014
56904
|
this._composeFileName = (consumer, output, extension, executionId) => {
|
|
56015
56905
|
if (output.exportName && output.exportName.trim().length > 0) {
|
|
@@ -56041,7 +56931,7 @@ var init_ConsumerExecutor = __esm({
|
|
|
56041
56931
|
init_src();
|
|
56042
56932
|
init_src10();
|
|
56043
56933
|
init_OutputExecutor();
|
|
56044
|
-
|
|
56934
|
+
init_DatasetSchema();
|
|
56045
56935
|
init_src2();
|
|
56046
56936
|
LIMIT_SCAN_BYTES = 64 * 1024;
|
|
56047
56937
|
NEWLINE_BYTE = 10;
|
|
@@ -56165,13 +57055,20 @@ var init_ConsumerExecutor = __esm({
|
|
|
56165
57055
|
}
|
|
56166
57056
|
return output;
|
|
56167
57057
|
};
|
|
56168
|
-
|
|
56169
|
-
|
|
56170
|
-
|
|
56171
|
-
|
|
56172
|
-
|
|
56173
|
-
|
|
56174
|
-
|
|
57058
|
+
/**
|
|
57059
|
+
* Rewrites every `lineCount` field of the merged dataset so it names the line of the SOURCE file
|
|
57060
|
+
* the row came from, by adding each chunk's `rowOffset` to the counter its worker wrote.
|
|
57061
|
+
*
|
|
57062
|
+
* The shape is unchanged, so the schema comes back with a fresh `writtenBy` and the row count
|
|
57063
|
+
* this pass actually saw - which also checks the file still holds what the workers reported.
|
|
57064
|
+
*/
|
|
57065
|
+
this.processLineCount = async (consumer, dsSchema, datasetPath, executorResults) => {
|
|
57066
|
+
Affirm_default(dsSchema, "Invalid dataset schema");
|
|
57067
|
+
const lineCountColumns = dsSchema.columns.filter((column) => column.source === "lineCount");
|
|
57068
|
+
if (lineCountColumns.length === 0)
|
|
57069
|
+
return dsSchema;
|
|
57070
|
+
const parseLine = LineParser_default.datasetParser(dsSchema);
|
|
57071
|
+
const outputPlan = OutputExecutor_default.compileOutput(consumer, dsSchema);
|
|
56175
57072
|
const tempWorkPath = datasetPath + "_tmp";
|
|
56176
57073
|
const reader = import_fs21.default.createReadStream(datasetPath);
|
|
56177
57074
|
const writer = import_fs21.default.createWriteStream(tempWorkPath);
|
|
@@ -56193,11 +57090,11 @@ var init_ConsumerExecutor = __esm({
|
|
|
56193
57090
|
Affirm_default(currentWorker, `Unable to resolve worker metadata while processing lineCount for ${datasetPath}`);
|
|
56194
57091
|
const rowOffset = currentWorker.rowOffset ?? 0;
|
|
56195
57092
|
if (rowOffset > 0) {
|
|
56196
|
-
const record =
|
|
56197
|
-
for (const
|
|
56198
|
-
const currentValue = Number(record[
|
|
57093
|
+
const record = parseLine(line);
|
|
57094
|
+
for (const column of lineCountColumns) {
|
|
57095
|
+
const currentValue = Number(record[column.name]);
|
|
56199
57096
|
Affirm_default(!Number.isNaN(currentValue), `Invalid lineCount value found while processing ${datasetPath}`);
|
|
56200
|
-
record[
|
|
57097
|
+
record[column.name] = currentValue + rowOffset;
|
|
56201
57098
|
}
|
|
56202
57099
|
if (!writer.write(OutputExecutor_default.outputRecord(record, outputPlan) + "\n"))
|
|
56203
57100
|
await waitForDrain();
|
|
@@ -56220,11 +57117,11 @@ var init_ConsumerExecutor = __esm({
|
|
|
56220
57117
|
}
|
|
56221
57118
|
await import_promises16.default.unlink(datasetPath);
|
|
56222
57119
|
await import_promises16.default.rename(tempWorkPath, datasetPath);
|
|
56223
|
-
return processedLineCount;
|
|
57120
|
+
return DatasetSchema_default.rewritten(dsSchema, "lineCount", processedLineCount);
|
|
56224
57121
|
};
|
|
56225
57122
|
/**
|
|
56226
|
-
* Cuts the merged dataset down to the run's record limit, and reports the
|
|
56227
|
-
*
|
|
57123
|
+
* Cuts the merged dataset down to the run's record limit, and reports the shape it left behind.
|
|
57124
|
+
* The schema comes back unchanged when the dataset was already within the limit.
|
|
56228
57125
|
*
|
|
56229
57126
|
* Every worker stops at the limit within its OWN chunk, which is the right thing for the read
|
|
56230
57127
|
* (no thread reads further than the run could need) but leaves up to `limit` rows per chunk in
|
|
@@ -56234,7 +57131,8 @@ var init_ConsumerExecutor = __esm({
|
|
|
56234
57131
|
* at then needs no arithmetic on line contents, so no line ending or multi-byte character can
|
|
56235
57132
|
* put the cut in the middle of a row. Only the bytes up to the limit are ever read.
|
|
56236
57133
|
*/
|
|
56237
|
-
this.processLimit = async (datasetPath, limit) => {
|
|
57134
|
+
this.processLimit = async (dsSchema, datasetPath, limit) => {
|
|
57135
|
+
Affirm_default(dsSchema, "Invalid dataset schema");
|
|
56238
57136
|
Affirm_default(datasetPath, "Invalid dataset path");
|
|
56239
57137
|
Affirm_default(limit > 0, `Invalid record limit ${limit}`);
|
|
56240
57138
|
const handle = await import_promises16.default.open(datasetPath, "r+");
|
|
@@ -56245,7 +57143,7 @@ var init_ConsumerExecutor = __esm({
|
|
|
56245
57143
|
while (newlines < limit) {
|
|
56246
57144
|
const { bytesRead } = await handle.read(buffer, 0, LIMIT_SCAN_BYTES, offset);
|
|
56247
57145
|
if (bytesRead === 0)
|
|
56248
|
-
return
|
|
57146
|
+
return dsSchema;
|
|
56249
57147
|
for (let index = 0; index < bytesRead; index++) {
|
|
56250
57148
|
offset++;
|
|
56251
57149
|
if (buffer[index] !== NEWLINE_BYTE) continue;
|
|
@@ -56255,14 +57153,15 @@ var init_ConsumerExecutor = __esm({
|
|
|
56255
57153
|
}
|
|
56256
57154
|
const { size } = await handle.stat();
|
|
56257
57155
|
if (offset >= size)
|
|
56258
|
-
return
|
|
57156
|
+
return dsSchema;
|
|
56259
57157
|
await handle.truncate(offset);
|
|
56260
|
-
return limit;
|
|
57158
|
+
return DatasetSchema_default.rewritten(dsSchema, "limit", limit);
|
|
56261
57159
|
} finally {
|
|
56262
57160
|
await handle.close();
|
|
56263
57161
|
}
|
|
56264
57162
|
};
|
|
56265
|
-
this.processDistinct = async (datasetPath) => {
|
|
57163
|
+
this.processDistinct = async (dsSchema, datasetPath) => {
|
|
57164
|
+
Affirm_default(dsSchema, "Invalid dataset schema");
|
|
56266
57165
|
const reader = import_fs21.default.createReadStream(datasetPath);
|
|
56267
57166
|
const tempWorkPath = datasetPath + "_tmp";
|
|
56268
57167
|
const writer = import_fs21.default.createWriteStream(tempWorkPath);
|
|
@@ -56291,18 +57190,18 @@ var init_ConsumerExecutor = __esm({
|
|
|
56291
57190
|
}
|
|
56292
57191
|
await import_promises16.default.unlink(datasetPath);
|
|
56293
57192
|
await import_promises16.default.rename(tempWorkPath, datasetPath);
|
|
56294
|
-
return newLineCount;
|
|
57193
|
+
return DatasetSchema_default.rewritten(dsSchema, "distinct", newLineCount);
|
|
56295
57194
|
};
|
|
56296
|
-
this.processDistinctOn = async (consumer, datasetPath) => {
|
|
57195
|
+
this.processDistinctOn = async (consumer, dsSchema, datasetPath) => {
|
|
57196
|
+
Affirm_default(dsSchema, "Invalid dataset schema");
|
|
56297
57197
|
const reader = import_fs21.default.createReadStream(datasetPath);
|
|
56298
57198
|
const { distinctOn } = consumer.options;
|
|
56299
57199
|
const { keys, resolution } = distinctOn;
|
|
56300
57200
|
const { strategy, orderBy, direction = "asc" } = resolution;
|
|
56301
|
-
const
|
|
56302
|
-
const internalFields = ConsumerManager_default.getExpandedFields(consumer);
|
|
57201
|
+
const parseLine = LineParser_default.datasetParser(dsSchema);
|
|
56303
57202
|
const winners = /* @__PURE__ */ new Map();
|
|
56304
57203
|
for await (const line of readLines(reader)) {
|
|
56305
|
-
const record =
|
|
57204
|
+
const record = parseLine(line);
|
|
56306
57205
|
const compositeKey = keys.map((k7) => String(record[k7] ?? "")).join("|");
|
|
56307
57206
|
const existing = winners.get(compositeKey);
|
|
56308
57207
|
if (!existing) {
|
|
@@ -56334,20 +57233,27 @@ var init_ConsumerExecutor = __esm({
|
|
|
56334
57233
|
}
|
|
56335
57234
|
await import_promises16.default.unlink(datasetPath);
|
|
56336
57235
|
await import_promises16.default.rename(tempWorkPath, datasetPath);
|
|
56337
|
-
return winners.size;
|
|
57236
|
+
return DatasetSchema_default.rewritten(dsSchema, "distinctOn", winners.size);
|
|
56338
57237
|
};
|
|
56339
|
-
|
|
57238
|
+
/**
|
|
57239
|
+
* Groups the dataset by `rowKeys` and turns one column's values into columns of their own.
|
|
57240
|
+
*
|
|
57241
|
+
* This is the one pass that changes the SHAPE of the file, which is why it returns the schema it
|
|
57242
|
+
* wrote rather than a row count: the header, the split pass, the dataset validation and the
|
|
57243
|
+
* parquet schema all read the result.
|
|
57244
|
+
*/
|
|
57245
|
+
this.processPivot = async (consumer, dsSchema, datasetPath) => {
|
|
57246
|
+
Affirm_default(dsSchema, "Invalid dataset schema");
|
|
56340
57247
|
const { pivot } = consumer.options;
|
|
56341
57248
|
const { rowKeys, pivotColumn, valueColumn, aggregation, columnPrefix = "" } = pivot;
|
|
56342
|
-
const
|
|
56343
|
-
const internalFields = ConsumerManager_default.getExpandedFields(consumer);
|
|
57249
|
+
const parseLine = LineParser_default.datasetParser(dsSchema);
|
|
56344
57250
|
let pivotValues = pivot.pivotValues;
|
|
56345
57251
|
if (!pivotValues) {
|
|
56346
57252
|
pivotValues = [];
|
|
56347
57253
|
const discoverySet = /* @__PURE__ */ new Set();
|
|
56348
57254
|
const discoverReader = import_fs21.default.createReadStream(datasetPath);
|
|
56349
57255
|
for await (const line of readLines(discoverReader)) {
|
|
56350
|
-
const record =
|
|
57256
|
+
const record = parseLine(line);
|
|
56351
57257
|
const val = String(record[pivotColumn] ?? "");
|
|
56352
57258
|
if (!discoverySet.has(val)) {
|
|
56353
57259
|
discoverySet.add(val);
|
|
@@ -56364,7 +57270,7 @@ var init_ConsumerExecutor = __esm({
|
|
|
56364
57270
|
const groups = /* @__PURE__ */ new Map();
|
|
56365
57271
|
const reader = import_fs21.default.createReadStream(datasetPath);
|
|
56366
57272
|
for await (const line of readLines(reader)) {
|
|
56367
|
-
const record =
|
|
57273
|
+
const record = parseLine(line);
|
|
56368
57274
|
const compositeKey = rowKeys.map((k7) => String(record[k7] ?? "")).join("|");
|
|
56369
57275
|
const pivotVal = String(record[pivotColumn] ?? "");
|
|
56370
57276
|
const numericVal = Number(record[valueColumn]) || 0;
|
|
@@ -56383,11 +57289,8 @@ var init_ConsumerExecutor = __esm({
|
|
|
56383
57289
|
cell.min = Math.min(cell.min, numericVal);
|
|
56384
57290
|
cell.max = Math.max(cell.max, numericVal);
|
|
56385
57291
|
}
|
|
56386
|
-
const
|
|
56387
|
-
|
|
56388
|
-
...pivotValues.map((pv) => ({ cField: { key: columnPrefix + pv }, finalKey: columnPrefix + pv }))
|
|
56389
|
-
];
|
|
56390
|
-
const outputPlan = OutputExecutor_default.compileOutput(consumer, pivotedFields);
|
|
57292
|
+
const pivotedDsSchema = DatasetSchema_default.forPivot(dsSchema, rowKeys, pivotValues.map((pv) => columnPrefix + pv), groups.size);
|
|
57293
|
+
const outputPlan = OutputExecutor_default.compileOutput(consumer, pivotedDsSchema);
|
|
56391
57294
|
const tempWorkPath = datasetPath + "_tmp";
|
|
56392
57295
|
const writer = import_fs21.default.createWriteStream(tempWorkPath);
|
|
56393
57296
|
const waitForDrain = () => new Promise((resolve) => writer.once("drain", resolve));
|
|
@@ -56437,10 +57340,8 @@ var init_ConsumerExecutor = __esm({
|
|
|
56437
57340
|
}
|
|
56438
57341
|
await import_promises16.default.unlink(datasetPath);
|
|
56439
57342
|
await import_promises16.default.rename(tempWorkPath, datasetPath);
|
|
56440
|
-
|
|
56441
|
-
|
|
56442
|
-
this._parseLine = (line, format4, fields) => {
|
|
56443
|
-
return format4 === "CSV" ? LineParser_default._internalParseCSV(line, fields) : LineParser_default._internalParseJSON(line);
|
|
57343
|
+
Affirm_default(outputCount === pivotedDsSchema.rows, `Pivot wrote ${outputCount} row(s) but declared ${pivotedDsSchema.rows}`);
|
|
57344
|
+
return pivotedDsSchema;
|
|
56444
57345
|
};
|
|
56445
57346
|
/**
|
|
56446
57347
|
* Determines if the new record should replace the existing record based on the resolution strategy
|
|
@@ -56467,11 +57368,11 @@ var init_ConsumerExecutor = __esm({
|
|
|
56467
57368
|
return false;
|
|
56468
57369
|
}
|
|
56469
57370
|
};
|
|
56470
|
-
this.processDatasetValidation = async (consumer, datasetPath) => {
|
|
57371
|
+
this.processDatasetValidation = async (consumer, dsSchema, datasetPath) => {
|
|
57372
|
+
Affirm_default(dsSchema, "Invalid dataset schema");
|
|
56471
57373
|
const validations = consumer.validate;
|
|
56472
57374
|
if (!validations || validations.length === 0) return [];
|
|
56473
|
-
const
|
|
56474
|
-
const internalFields = ConsumerManager_default.getExpandedFields(consumer);
|
|
57375
|
+
const parseLine = LineParser_default.datasetParser(dsSchema);
|
|
56475
57376
|
let rowCount = 0;
|
|
56476
57377
|
const seenRows = /* @__PURE__ */ new Set();
|
|
56477
57378
|
const fieldValueSets = /* @__PURE__ */ new Map();
|
|
@@ -56492,7 +57393,7 @@ var init_ConsumerExecutor = __esm({
|
|
|
56492
57393
|
seenRows.add(line);
|
|
56493
57394
|
}
|
|
56494
57395
|
if (uniqueFieldKeys.length > 0) {
|
|
56495
|
-
const record =
|
|
57396
|
+
const record = parseLine(line);
|
|
56496
57397
|
for (const fieldKey of uniqueFieldKeys) {
|
|
56497
57398
|
const valueSet = fieldValueSets.get(fieldKey);
|
|
56498
57399
|
const val = String(record[fieldKey] ?? "");
|
|
@@ -56629,14 +57530,13 @@ var init_ExecutorPerformance = __esm({
|
|
|
56629
57530
|
});
|
|
56630
57531
|
|
|
56631
57532
|
// ../../packages/executors/src/ExecutorWriter.ts
|
|
56632
|
-
var
|
|
57533
|
+
var fs28, ExecutorWriter, ExecutorWriter_default;
|
|
56633
57534
|
var init_ExecutorWriter = __esm({
|
|
56634
57535
|
"../../packages/executors/src/ExecutorWriter.ts"() {
|
|
56635
|
-
|
|
57536
|
+
fs28 = __toESM(require("fs"));
|
|
56636
57537
|
init_src();
|
|
56637
57538
|
init_src3();
|
|
56638
57539
|
init_src10();
|
|
56639
|
-
init_OutputExecutor();
|
|
56640
57540
|
ExecutorWriter = class {
|
|
56641
57541
|
constructor() {
|
|
56642
57542
|
/**
|
|
@@ -56666,7 +57566,8 @@ var init_ExecutorWriter = __esm({
|
|
|
56666
57566
|
Affirm_default(request, "Invalid split request");
|
|
56667
57567
|
Affirm_default(request.sourcePath, "Missing dataset to split");
|
|
56668
57568
|
Affirm_default(request.consumer, "Invalid consumer");
|
|
56669
|
-
const { scope, sourcePath, consumer,
|
|
57569
|
+
const { scope, sourcePath, consumer, dsSchema } = request;
|
|
57570
|
+
Affirm_default(dsSchema, "Missing dataset schema to split against");
|
|
56670
57571
|
const keys = scope.split?.by ?? [];
|
|
56671
57572
|
const maxFileBytes = scope.split?.maxFileSizeGb > 0 ? scope.split.maxFileSizeGb * this.BYTES_PER_GB : null;
|
|
56672
57573
|
if (keys.length === 0 && !maxFileBytes)
|
|
@@ -56681,21 +57582,21 @@ var init_ExecutorWriter = __esm({
|
|
|
56681
57582
|
usedLabels: /* @__PURE__ */ new Set(),
|
|
56682
57583
|
fileCount: 0
|
|
56683
57584
|
};
|
|
56684
|
-
await this._route(pass,
|
|
57585
|
+
await this._route(pass, dsSchema);
|
|
56685
57586
|
if (pass.buckets.size === 0)
|
|
56686
57587
|
return [{ path: sourcePath, label: this.UNSPLIT_LABEL }];
|
|
56687
|
-
await
|
|
57588
|
+
await fs28.promises.unlink(sourcePath);
|
|
56688
57589
|
return this._listFiles(pass);
|
|
56689
57590
|
};
|
|
56690
57591
|
/**
|
|
56691
57592
|
* The pass itself: one line read, one line written, for every line of the dataset.
|
|
56692
57593
|
*/
|
|
56693
|
-
this._route = async (pass,
|
|
56694
|
-
const
|
|
56695
|
-
const readStream =
|
|
57594
|
+
this._route = async (pass, dsSchema) => {
|
|
57595
|
+
const parseLine = pass.keys.length === 0 ? null : LineParser_default.datasetParser(dsSchema);
|
|
57596
|
+
const readStream = fs28.createReadStream(pass.sourcePath);
|
|
56696
57597
|
try {
|
|
56697
57598
|
for await (const line of readLines(readStream)) {
|
|
56698
|
-
const bucket = this._bucketFor(line, pass,
|
|
57599
|
+
const bucket = this._bucketFor(line, pass, parseLine);
|
|
56699
57600
|
const lineBytes = Buffer.byteLength(line) + 1;
|
|
56700
57601
|
if (this._needsRotation(bucket, pass, lineBytes))
|
|
56701
57602
|
await this._rotate(bucket, pass);
|
|
@@ -56715,8 +57616,8 @@ var init_ExecutorWriter = __esm({
|
|
|
56715
57616
|
* own: a shared catch-all would silently merge two different things, and that is invisible until
|
|
56716
57617
|
* it is wrong.
|
|
56717
57618
|
*/
|
|
56718
|
-
this._bucketFor = (line, pass,
|
|
56719
|
-
const bucketKey =
|
|
57619
|
+
this._bucketFor = (line, pass, parseLine) => {
|
|
57620
|
+
const bucketKey = parseLine ? this._bucketKey(line, pass.keys, parseLine) : "";
|
|
56720
57621
|
const existing = pass.buckets.get(bucketKey);
|
|
56721
57622
|
if (existing) return existing;
|
|
56722
57623
|
const bucket = {
|
|
@@ -56730,8 +57631,8 @@ var init_ExecutorWriter = __esm({
|
|
|
56730
57631
|
return bucket;
|
|
56731
57632
|
};
|
|
56732
57633
|
/** The composite value a line buckets on, read off the parsed record. */
|
|
56733
|
-
this._bucketKey = (line, keys,
|
|
56734
|
-
const record =
|
|
57634
|
+
this._bucketKey = (line, keys, parseLine) => {
|
|
57635
|
+
const record = parseLine(line);
|
|
56735
57636
|
return keys.map((key) => {
|
|
56736
57637
|
const value = record[key];
|
|
56737
57638
|
if (value === null || value === void 0) return "_null";
|
|
@@ -56796,7 +57697,7 @@ var init_ExecutorWriter = __esm({
|
|
|
56796
57697
|
}
|
|
56797
57698
|
if (pass.openStreams.length >= Constants_default.defaults.MAX_OPEN_SPLIT_STREAMS)
|
|
56798
57699
|
await this._closeStream(pass.openStreams.shift());
|
|
56799
|
-
bucket.stream =
|
|
57700
|
+
bucket.stream = fs28.createWriteStream(this.getCompletedPath(pass.sourcePath, this._composeLabel(bucket, pass)), { flags: "a" });
|
|
56800
57701
|
pass.openStreams.push(bucket);
|
|
56801
57702
|
return bucket.stream;
|
|
56802
57703
|
};
|
|
@@ -57085,7 +57986,6 @@ var init_LookupResolver = __esm({
|
|
|
57085
57986
|
init_src4();
|
|
57086
57987
|
init_src10();
|
|
57087
57988
|
init_src2();
|
|
57088
|
-
init_OutputExecutor();
|
|
57089
57989
|
LookupResolverClass = class {
|
|
57090
57990
|
constructor() {
|
|
57091
57991
|
// Reuse dicts across sibling consumers within one process; invalidated when the config changes.
|
|
@@ -57108,28 +58008,35 @@ var init_LookupResolver = __esm({
|
|
|
57108
58008
|
Affirm_default(!stack.includes(ref.consumer), `code_lookup cycle detected: ${[...stack, ref.consumer].join(" -> ")}`);
|
|
57109
58009
|
const lookupConsumer = Environment_default.getConsumer(ref.consumer);
|
|
57110
58010
|
Affirm_default(lookupConsumer, `code_lookup references unknown consumer "${ref.consumer}"`);
|
|
57111
|
-
let
|
|
57112
|
-
if (!
|
|
57113
|
-
capturePath = this.getCapturePath(scope, ref.consumer);
|
|
58011
|
+
let capture = captureByConsumer.get(ref.consumer);
|
|
58012
|
+
if (!capture) {
|
|
58013
|
+
const capturePath = this.getCapturePath(scope, ref.consumer);
|
|
57114
58014
|
ExecutorScope_default.ensurePath(capturePath);
|
|
57115
|
-
|
|
57116
|
-
|
|
58015
|
+
capture = {
|
|
58016
|
+
path: capturePath,
|
|
58017
|
+
dsSchema: await this.runLookupConsumer(lookupConsumer, capturePath, stack, request)
|
|
58018
|
+
};
|
|
58019
|
+
captureByConsumer.set(ref.consumer, capture);
|
|
57117
58020
|
}
|
|
57118
|
-
this.cache.set(id, await this.buildDict(
|
|
58021
|
+
this.cache.set(id, await this.buildDict(capture, ref));
|
|
57119
58022
|
}
|
|
57120
58023
|
ExecutorScope_default.ensurePath(outPath);
|
|
57121
58024
|
await import_promises17.default.writeFile(outPath, JSON.stringify(this.cache.get(id)));
|
|
57122
58025
|
manifest.push({ id, consumer: ref.consumer, key: ref.key, value: ref.value, path: import_path22.default.resolve(outPath) });
|
|
57123
58026
|
}
|
|
57124
|
-
for (const
|
|
57125
|
-
await import_promises17.default.rm(
|
|
58027
|
+
for (const capture of captureByConsumer.values())
|
|
58028
|
+
await import_promises17.default.rm(capture.path, { force: true });
|
|
57126
58029
|
return manifest;
|
|
57127
58030
|
};
|
|
57128
|
-
|
|
58031
|
+
/**
|
|
58032
|
+
* Runs the source consumer through the real pipeline, capturing its merged output before
|
|
58033
|
+
* cleanup and returning the shape that run produced. The capture is a verbatim copy of the
|
|
58034
|
+
* dataset, so its schema is the ONLY description of it.
|
|
58035
|
+
*/
|
|
57129
58036
|
this.runLookupConsumer = async (lookupConsumer, capturePath, stack, request) => {
|
|
57130
58037
|
const { default: ExecutorOrchestrator2 } = await Promise.resolve().then(() => (init_ExecutorOrchestrator(), ExecutorOrchestrator_exports));
|
|
57131
58038
|
Logger_default.log(`[lookup] Materializing consumer "${lookupConsumer.name}" (path: ${stack.join(" -> ")})`);
|
|
57132
|
-
await ExecutorOrchestrator2.launch({
|
|
58039
|
+
const result = await ExecutorOrchestrator2.launch({
|
|
57133
58040
|
consumer: lookupConsumer,
|
|
57134
58041
|
details: request.details,
|
|
57135
58042
|
captureMainDatasetTo: capturePath,
|
|
@@ -57137,22 +58044,23 @@ var init_LookupResolver = __esm({
|
|
|
57137
58044
|
_lookupStack: stack,
|
|
57138
58045
|
syntheticProducerInputs: request.syntheticProducerInputs
|
|
57139
58046
|
});
|
|
58047
|
+
Affirm_default(result.dsSchema, `code_lookup source consumer "${lookupConsumer.name}" reported no dataset schema`);
|
|
58048
|
+
return result.dsSchema;
|
|
57140
58049
|
};
|
|
57141
58050
|
// Projects the captured dataset to a { code: value } dict, bounded by MAX_LOOKUP_ROWS.
|
|
57142
|
-
this.buildDict = async (
|
|
57143
|
-
const
|
|
57144
|
-
|
|
57145
|
-
|
|
57146
|
-
|
|
57147
|
-
Affirm_default(finalKeys.includes(ref.value), `code_lookup value column "${ref.value}" is not in the output of consumer "${ref.consumer}"`);
|
|
58051
|
+
this.buildDict = async (capture, ref) => {
|
|
58052
|
+
const columnNames = capture.dsSchema.columns.map((column) => column.name);
|
|
58053
|
+
Affirm_default(columnNames.includes(ref.key), `code_lookup key column "${ref.key}" is not in the output of consumer "${ref.consumer}"`);
|
|
58054
|
+
Affirm_default(columnNames.includes(ref.value), `code_lookup value column "${ref.value}" is not in the output of consumer "${ref.consumer}"`);
|
|
58055
|
+
const parseLine = LineParser_default.datasetParser(capture.dsSchema);
|
|
57148
58056
|
const dict = {};
|
|
57149
58057
|
let rowsRead = 0;
|
|
57150
58058
|
let warnedDup = false;
|
|
57151
|
-
for await (const line of readLines(import_fs23.default.createReadStream(
|
|
58059
|
+
for await (const line of readLines(import_fs23.default.createReadStream(capture.path))) {
|
|
57152
58060
|
if (!line.length) continue;
|
|
57153
58061
|
rowsRead++;
|
|
57154
58062
|
Affirm_default(rowsRead <= Constants_default.defaults.MAX_LOOKUP_ROWS, `code_lookup table from consumer "${ref.consumer}" exceeds MAX_LOOKUP_ROWS (${Constants_default.defaults.MAX_LOOKUP_ROWS})`);
|
|
57155
|
-
const record =
|
|
58063
|
+
const record = parseLine(line);
|
|
57156
58064
|
const code = String(record[ref.key]);
|
|
57157
58065
|
if (code in dict && !warnedDup) {
|
|
57158
58066
|
Logger_default.warn(`code_lookup: duplicate key(s) in consumer "${ref.consumer}" for "${ref.key}"; last value wins`);
|
|
@@ -57163,10 +58071,6 @@ var init_LookupResolver = __esm({
|
|
|
57163
58071
|
Logger_default.log(`[lookup] Built table "${ref.consumer}" (${ref.key}->${ref.value}): ${Object.keys(dict).length} entr(ies) from ${rowsRead} row(s)`);
|
|
57164
58072
|
return dict;
|
|
57165
58073
|
};
|
|
57166
|
-
this.parseLine = (line, format4, visibleFields) => {
|
|
57167
|
-
if (format4 === "CSV") return LineParser_default._internalParseCSV(line, visibleFields, ",");
|
|
57168
|
-
return JSON.parse(line);
|
|
57169
|
-
};
|
|
57170
58074
|
this.getCapturePath = (scope, consumerName) => {
|
|
57171
58075
|
const dir = import_path22.default.dirname(ExecutorScope_default.getLookupPath(scope, "x"));
|
|
57172
58076
|
return import_path22.default.join(dir, `${consumerName}.capture`);
|
|
@@ -57200,7 +58104,6 @@ var init_ConsumerInputResolver = __esm({
|
|
|
57200
58104
|
init_src();
|
|
57201
58105
|
init_src3();
|
|
57202
58106
|
init_src4();
|
|
57203
|
-
init_src10();
|
|
57204
58107
|
init_src2();
|
|
57205
58108
|
ConsumerInputResolverClass = class {
|
|
57206
58109
|
constructor() {
|
|
@@ -57225,23 +58128,29 @@ var init_ConsumerInputResolver = __esm({
|
|
|
57225
58128
|
Affirm_default(upstream, `consumer-as-input references unknown consumer "${name}"`);
|
|
57226
58129
|
const capturePath = ExecutorScope_default.getConsumerInputPath(scope, name);
|
|
57227
58130
|
ExecutorScope_default.ensurePath(capturePath);
|
|
57228
|
-
await this.runUpstreamConsumer(upstream, capturePath, stack, request);
|
|
58131
|
+
const dsSchema = await this.runUpstreamConsumer(upstream, capturePath, stack, request);
|
|
57229
58132
|
const { size } = await import_promises18.default.stat(capturePath);
|
|
57230
|
-
this.cache.set(name, { consumer: name, path: import_path23.default.resolve(capturePath), fileSize: size });
|
|
58133
|
+
this.cache.set(name, { consumer: name, path: import_path23.default.resolve(capturePath), fileSize: size, dsSchema });
|
|
57231
58134
|
}
|
|
57232
58135
|
manifest.set(name, this.cache.get(name));
|
|
57233
58136
|
}
|
|
57234
58137
|
return manifest;
|
|
57235
58138
|
};
|
|
57236
|
-
|
|
57237
|
-
|
|
57238
|
-
|
|
57239
|
-
|
|
57240
|
-
|
|
58139
|
+
/**
|
|
58140
|
+
* Builds the in-memory producer the orchestrator reads the JSONL capture through. Its dimensions
|
|
58141
|
+
* are the columns the upstream run REPORTED writing, not the consumer's declared fields, which a
|
|
58142
|
+
* `pivot` leaves describing a shape no longer in the file.
|
|
58143
|
+
*
|
|
58144
|
+
* No `format` is carried over: the capture holds values the upstream run already rendered, so
|
|
58145
|
+
* re-parsing them with that dimension's input format would fail on its own output.
|
|
58146
|
+
*/
|
|
58147
|
+
this.buildSyntheticProducer = (entry) => {
|
|
58148
|
+
Affirm_default(entry, "Invalid consumer input entry");
|
|
58149
|
+
Affirm_default(entry.dsSchema, `Consumer input "${entry.consumer}" carries no dataset schema`);
|
|
57241
58150
|
return {
|
|
57242
|
-
name:
|
|
57243
|
-
source: `__consumer_input__:${
|
|
57244
|
-
dimensions,
|
|
58151
|
+
name: entry.consumer,
|
|
58152
|
+
source: `__consumer_input__:${entry.consumer}`,
|
|
58153
|
+
dimensions: entry.dsSchema.columns.map((column) => ({ name: column.name, type: column.type })),
|
|
57245
58154
|
settings: { fileType: "JSONL" },
|
|
57246
58155
|
_version: 1
|
|
57247
58156
|
};
|
|
@@ -57256,11 +58165,12 @@ var init_ConsumerInputResolver = __esm({
|
|
|
57256
58165
|
}
|
|
57257
58166
|
return names;
|
|
57258
58167
|
};
|
|
57259
|
-
// Runs the upstream consumer through the real pipeline, capturing its
|
|
58168
|
+
// Runs the upstream consumer through the real pipeline, capturing its output as JSONL and
|
|
58169
|
+
// reporting the shape that run produced.
|
|
57260
58170
|
this.runUpstreamConsumer = async (upstream, capturePath, stack, request) => {
|
|
57261
58171
|
const { default: ExecutorOrchestrator2 } = await Promise.resolve().then(() => (init_ExecutorOrchestrator(), ExecutorOrchestrator_exports));
|
|
57262
58172
|
Logger_default.log(`[consumer-input] Materializing consumer "${upstream.name}" (path: ${stack.join(" -> ")})`);
|
|
57263
|
-
await ExecutorOrchestrator2.launch({
|
|
58173
|
+
const result = await ExecutorOrchestrator2.launch({
|
|
57264
58174
|
consumer: upstream,
|
|
57265
58175
|
details: request.details,
|
|
57266
58176
|
captureMainDatasetTo: capturePath,
|
|
@@ -57269,6 +58179,8 @@ var init_ConsumerInputResolver = __esm({
|
|
|
57269
58179
|
_consumerInputStack: stack,
|
|
57270
58180
|
syntheticProducerInputs: request.syntheticProducerInputs
|
|
57271
58181
|
});
|
|
58182
|
+
Affirm_default(result.dsSchema, `consumer-as-input source "${upstream.name}" reported no dataset schema`);
|
|
58183
|
+
return result.dsSchema;
|
|
57272
58184
|
};
|
|
57273
58185
|
this.invalidateCacheIfStale = () => {
|
|
57274
58186
|
const generation = Environment_default._configHash ?? null;
|
|
@@ -57310,6 +58222,7 @@ var init_ExecutorOrchestrator = __esm({
|
|
|
57310
58222
|
init_src();
|
|
57311
58223
|
init_src10();
|
|
57312
58224
|
init_ExecutorWriter();
|
|
58225
|
+
init_DatasetSchema();
|
|
57313
58226
|
import_promises20 = require("stream/promises");
|
|
57314
58227
|
init_src2();
|
|
57315
58228
|
init_ExecutorProgress2();
|
|
@@ -57328,9 +58241,9 @@ var init_ExecutorOrchestrator = __esm({
|
|
|
57328
58241
|
* spawn lazily, on first exec, and a thread that dies mid-run is replaced by another, so this is
|
|
57329
58242
|
* a count of spawns and NOT a concurrency figure (see IWorkerCensus).
|
|
57330
58243
|
*
|
|
57331
|
-
* `workerData` carries the loaded environment
|
|
57332
|
-
*
|
|
57333
|
-
* data goes in the exec argument,
|
|
58244
|
+
* `workerData` carries the loaded environment and the run's dataset schema, so both are cloned
|
|
58245
|
+
* once per THREAD at spawn rather than per chunk. Only run-constant data belongs there; per-task
|
|
58246
|
+
* data goes in the exec argument, cloned per chunk.
|
|
57334
58247
|
*/
|
|
57335
58248
|
this.createPool = (maxWorkers, census, threadData2) => {
|
|
57336
58249
|
const options2 = {
|
|
@@ -57361,13 +58274,19 @@ var init_ExecutorOrchestrator = __esm({
|
|
|
57361
58274
|
const tracker = new ExecutorPerformance_default();
|
|
57362
58275
|
const _progress = new ExecutorProgress2_default(logProgress);
|
|
57363
58276
|
const { usageId } = UsageManager_default.startUsage(consumer, details);
|
|
57364
|
-
const scope = { id: usageId, folder: `${consumer.name}_${usageId}`, workersId: [], split: OrchestratorHelper_default.resolveSplit(consumer) };
|
|
58277
|
+
const scope = ExecutorScope_default.open({ id: usageId, folder: `${consumer.name}_${usageId}`, workersId: [], split: OrchestratorHelper_default.resolveSplit(consumer) });
|
|
57365
58278
|
const expandedFields = ConsumerManager_default.getExpandedFields(consumer);
|
|
58279
|
+
let dsSchema = DatasetSchema_default.compile(consumer, expandedFields);
|
|
57366
58280
|
const census = { created: 0, peak: 0 };
|
|
57367
58281
|
try {
|
|
57368
58282
|
const start = performance.now();
|
|
57369
58283
|
const executorResults = [];
|
|
57370
58284
|
Logger_default.info(`[${usageId}] Launching consumer "${consumer.name}" (invoked by: ${details.invokedBy}, user: ${details.user?.name ?? "unknown"}, producer(s): ${consumer.producers.length}${OrchestratorHelper_default.formatRequestOptions(options2)}) | ${OrchestratorHelper_default.formatMemoryUsage()}`);
|
|
58285
|
+
const fixedReview = request.skipExport ? null : OutputExecutor_default.reviewFixedOutput(consumer, dsSchema);
|
|
58286
|
+
Affirm_default(
|
|
58287
|
+
!fixedReview || fixedReview.errors.length === 0,
|
|
58288
|
+
`Consumer "${consumer.name}" cannot write its FIXED output: ${fixedReview?.errors.join(" ")}`
|
|
58289
|
+
);
|
|
57371
58290
|
const lookups = await LookupResolver_default.materialize(consumer, expandedFields, scope, request);
|
|
57372
58291
|
const consumerInputs = await ConsumerInputResolver_default.materialize(consumer, scope, request);
|
|
57373
58292
|
let counter = performance.now();
|
|
@@ -57397,6 +58316,7 @@ var init_ExecutorOrchestrator = __esm({
|
|
|
57397
58316
|
executorResults.push(...await this.runWorkFiles({
|
|
57398
58317
|
workFiles,
|
|
57399
58318
|
consumer,
|
|
58319
|
+
dsSchema,
|
|
57400
58320
|
scope,
|
|
57401
58321
|
options: options2,
|
|
57402
58322
|
lookups,
|
|
@@ -57411,58 +58331,26 @@ var init_ExecutorOrchestrator = __esm({
|
|
|
57411
58331
|
Logger_default.log(`[${usageId}] All workers complete \u2014 ${executorResults.length} result(s), total input: ${Algo_default.sum(executorResults.map((x2) => x2.inputCount))}, total output: ${Algo_default.sum(executorResults.map((x2) => x2.outputCount))}`);
|
|
57412
58332
|
_progress.update({ phase: "Reconciling work", progress: 0 });
|
|
57413
58333
|
await this.reconcileExecutorThreadsResults(scope, executorResults, tracker);
|
|
58334
|
+
dsSchema = DatasetSchema_default.rewritten(dsSchema, "merge", Algo_default.sum(executorResults.map((x2) => x2.outputCount)));
|
|
57414
58335
|
_progress.update({ phase: "Reconciling work", progress: 1 });
|
|
57415
58336
|
Logger_default.log(`[${usageId}] Reconciled ${executorResults.length} worker result(s)`);
|
|
57416
58337
|
_progress.update({ phase: "Post-processing", progress: 0 });
|
|
57417
|
-
|
|
57418
|
-
if (executorResults.length > 1) {
|
|
57419
|
-
if (consumer.options?.distinct === true) {
|
|
57420
|
-
Logger_default.log(`[${usageId}] Running unified distinct pass across merged workers`);
|
|
57421
|
-
counter = performance.now();
|
|
57422
|
-
const unifiedOutputCount = await ConsumerExecutor_default.processDistinct(ExecutorScope_default.getMainPath(scope));
|
|
57423
|
-
tracker.measure("process-distinct:main", performance.now() - counter);
|
|
57424
|
-
postOperation.totalOutputCount = unifiedOutputCount;
|
|
57425
|
-
Logger_default.log(`[${usageId}] Distinct pass complete: ${unifiedOutputCount} unique rows in ${Math.round(performance.now() - counter)}ms`);
|
|
57426
|
-
}
|
|
57427
|
-
if (consumer.options?.distinctOn) {
|
|
57428
|
-
Logger_default.log(`[${usageId}] Running unified distinctOn pass (${consumer.options.distinctOn})`);
|
|
57429
|
-
counter = performance.now();
|
|
57430
|
-
const unifiedOutputCount = await ConsumerExecutor_default.processDistinctOn(consumer, ExecutorScope_default.getMainPath(scope));
|
|
57431
|
-
tracker.measure("process-distinct-on:main", performance.now() - counter);
|
|
57432
|
-
postOperation.totalOutputCount = unifiedOutputCount;
|
|
57433
|
-
Logger_default.log(`[${usageId}] DistinctOn pass complete: ${unifiedOutputCount} rows in ${Math.round(performance.now() - counter)}ms`);
|
|
57434
|
-
}
|
|
57435
|
-
if (consumer.options?.pivot) {
|
|
57436
|
-
Logger_default.log(`[${usageId}] Running pivot operation`);
|
|
57437
|
-
counter = performance.now();
|
|
57438
|
-
const unifiedOutputCount = await ConsumerExecutor_default.processPivot(consumer, ExecutorScope_default.getMainPath(scope));
|
|
57439
|
-
tracker.measure("process-pivot:main", performance.now() - counter);
|
|
57440
|
-
postOperation.totalOutputCount = unifiedOutputCount;
|
|
57441
|
-
Logger_default.log(`[${usageId}] Pivot complete: ${unifiedOutputCount} rows in ${Math.round(performance.now() - counter)}ms`);
|
|
57442
|
-
}
|
|
57443
|
-
if (ConsumerManager_default.hasLineCountFields(consumer, true)) {
|
|
57444
|
-
Logger_default.log(`[${usageId}] Running unified lineCount pass across merged workers`);
|
|
57445
|
-
counter = performance.now();
|
|
57446
|
-
const processedLineCount = await ConsumerExecutor_default.processLineCount(consumer, ExecutorScope_default.getMainPath(scope), executorResults);
|
|
57447
|
-
tracker.measure("process-line-count:main", performance.now() - counter);
|
|
57448
|
-
Logger_default.log(`[${usageId}] LineCount pass complete: normalized ${processedLineCount} rows in ${Math.round(performance.now() - counter)}ms`);
|
|
57449
|
-
}
|
|
57450
|
-
}
|
|
58338
|
+
dsSchema = await this.runPostPasses({ consumer, dsSchema, scope, executorResults, tracker, usageId });
|
|
57451
58339
|
if (options2?.limit > 0) {
|
|
57452
58340
|
counter = performance.now();
|
|
57453
|
-
const
|
|
58341
|
+
const limited = await ConsumerExecutor_default.processLimit(dsSchema, ExecutorScope_default.getMainPath(scope), options2.limit);
|
|
57454
58342
|
tracker.measure("process-limit:main", performance.now() - counter);
|
|
57455
|
-
if (
|
|
57456
|
-
|
|
57457
|
-
Logger_default.log(`[${usageId}] Record limit applied: dataset cut to ${limitedOutputCount} row(s) in ${Math.round(performance.now() - counter)}ms`);
|
|
58343
|
+
if (limited.writtenBy === "limit") {
|
|
58344
|
+
Logger_default.log(`[${usageId}] Record limit applied: dataset cut to ${limited.rows} row(s) in ${Math.round(performance.now() - counter)}ms`);
|
|
57458
58345
|
} else {
|
|
57459
58346
|
Logger_default.log(`[${usageId}] Record limit of ${options2.limit} not reached \u2014 dataset left whole`);
|
|
57460
58347
|
}
|
|
58348
|
+
dsSchema = limited;
|
|
57461
58349
|
}
|
|
57462
58350
|
if (consumer.validate && consumer.validate.length > 0) {
|
|
57463
58351
|
Logger_default.log(`[${usageId}] Running dataset-level validations`);
|
|
57464
58352
|
counter = performance.now();
|
|
57465
|
-
const validationResults = await ConsumerExecutor_default.processDatasetValidation(consumer, ExecutorScope_default.getMainPath(scope));
|
|
58353
|
+
const validationResults = await ConsumerExecutor_default.processDatasetValidation(consumer, dsSchema, ExecutorScope_default.getMainPath(scope));
|
|
57466
58354
|
tracker.measure("dataset-validation", performance.now() - counter);
|
|
57467
58355
|
for (const result of validationResults) {
|
|
57468
58356
|
if (result.onFail === "fail") {
|
|
@@ -57476,7 +58364,7 @@ var init_ExecutorOrchestrator = __esm({
|
|
|
57476
58364
|
Logger_default.log(`[${usageId}] Dataset validations complete in ${Math.round(performance.now() - counter)}ms`);
|
|
57477
58365
|
}
|
|
57478
58366
|
if (request.captureMainDatasetTo) {
|
|
57479
|
-
await this.captureDataset(
|
|
58367
|
+
await this.captureDataset(dsSchema, ExecutorScope_default.getMainPath(scope), request.captureMainDatasetTo, request.captureFormat);
|
|
57480
58368
|
Logger_default.log(`[${usageId}] Captured dataset to ${request.captureMainDatasetTo}${request.captureFormat ? ` (as ${request.captureFormat})` : ""}`);
|
|
57481
58369
|
}
|
|
57482
58370
|
if (request.skipExport) {
|
|
@@ -57485,8 +58373,8 @@ var init_ExecutorOrchestrator = __esm({
|
|
|
57485
58373
|
const materializeResult = OrchestratorHelper_default.computeFinalResult(tracker, executorResults, usageId, null);
|
|
57486
58374
|
materializeResult.elapsedMS = performance.now() - start;
|
|
57487
58375
|
materializeResult.peakWorkers = census.peak;
|
|
57488
|
-
|
|
57489
|
-
|
|
58376
|
+
materializeResult.outputCount = dsSchema.rows;
|
|
58377
|
+
materializeResult.dsSchema = dsSchema;
|
|
57490
58378
|
UsageManager_default.endUsage(usageId, materializeResult.outputCount, materializeResult);
|
|
57491
58379
|
await Logger_default.flush();
|
|
57492
58380
|
return materializeResult;
|
|
@@ -57496,7 +58384,7 @@ var init_ExecutorOrchestrator = __esm({
|
|
|
57496
58384
|
scope,
|
|
57497
58385
|
sourcePath: ExecutorScope_default.getMainPath(scope),
|
|
57498
58386
|
consumer,
|
|
57499
|
-
|
|
58387
|
+
dsSchema
|
|
57500
58388
|
});
|
|
57501
58389
|
tracker.measure("split-output", performance.now() - counter);
|
|
57502
58390
|
if (scope.split)
|
|
@@ -57505,7 +58393,7 @@ var init_ExecutorOrchestrator = __esm({
|
|
|
57505
58393
|
_progress.update({ phase: "Exporting result", progress: 0 });
|
|
57506
58394
|
counter = performance.now();
|
|
57507
58395
|
Logger_default.info(`[${usageId}] Exporting results to ${consumer.outputs.length} output(s)`);
|
|
57508
|
-
const exportRes = await OutputExecutor_default.exportResult(consumer,
|
|
58396
|
+
const exportRes = await OutputExecutor_default.exportResult(consumer, dsSchema, splitFiles);
|
|
57509
58397
|
tracker.measure("export-result", performance.now() - counter);
|
|
57510
58398
|
Affirm_default(exportRes.res, `Consumer "${consumer.name}" failed to write its output to "${consumer.outputs.map((x2) => x2.exportDestination).join(", ")}" \u2014 see the logged driver error for the cause`);
|
|
57511
58399
|
Logger_default.info(`[${usageId}] Export complete in ${Math.round(performance.now() - counter)}ms (key: ${exportRes.key})`);
|
|
@@ -57522,8 +58410,8 @@ var init_ExecutorOrchestrator = __esm({
|
|
|
57522
58410
|
const finalResult = OrchestratorHelper_default.computeFinalResult(tracker, executorResults, usageId, exportRes.key);
|
|
57523
58411
|
finalResult.elapsedMS = performance.now() - start;
|
|
57524
58412
|
finalResult.peakWorkers = census.peak;
|
|
57525
|
-
|
|
57526
|
-
|
|
58413
|
+
finalResult.outputCount = dsSchema.rows;
|
|
58414
|
+
finalResult.dsSchema = dsSchema;
|
|
57527
58415
|
UsageManager_default.endUsage(usageId, finalResult.outputCount, finalResult);
|
|
57528
58416
|
Logger_default.info(`[${usageId}] Consumer "${consumer.name}" completed: ${finalResult.outputCount} rows, ${finalResult.workerCount} chunk(s) on ${census.peak} thread(s), ${Math.round(finalResult.elapsedMS)}ms`);
|
|
57529
58417
|
await Logger_default.flush();
|
|
@@ -57601,7 +58489,10 @@ var init_ExecutorOrchestrator = __esm({
|
|
|
57601
58489
|
`Invalid worker pool size ${poolSize} \u2014 it must come from getParallelWorkerCount(), never from the work list`
|
|
57602
58490
|
);
|
|
57603
58491
|
let phase = performance.now();
|
|
57604
|
-
const pool = this.createPool(poolSize, census, {
|
|
58492
|
+
const pool = this.createPool(poolSize, census, {
|
|
58493
|
+
environment: Environment_default.createSnapshot(),
|
|
58494
|
+
dsSchema: request.dsSchema
|
|
58495
|
+
});
|
|
57605
58496
|
tracker.measure("pool-create", performance.now() - phase);
|
|
57606
58497
|
const context = {
|
|
57607
58498
|
consumer: request.consumer,
|
|
@@ -57824,24 +58715,23 @@ var init_ExecutorOrchestrator = __esm({
|
|
|
57824
58715
|
};
|
|
57825
58716
|
/**
|
|
57826
58717
|
* Copies the merged main.dataset out of the run scope for a materialization run. With
|
|
57827
|
-
* captureFormat 'JSONL' the output is normalized to newline-delimited JSON objects keyed by
|
|
57828
|
-
*
|
|
58718
|
+
* captureFormat 'JSONL' the output is normalized to newline-delimited JSON objects keyed by the
|
|
58719
|
+
* dataset's own column names — self-describing, so a synthetic producer can read it without a
|
|
57829
58720
|
* header row. main.dataset in JSON internal format is already JSONL (verbatim copy); CSV internal
|
|
57830
58721
|
* format has no header row, so each row is reprojected. Without a captureFormat it's a verbatim copy.
|
|
57831
58722
|
*/
|
|
57832
|
-
this.captureDataset = async (
|
|
58723
|
+
this.captureDataset = async (dsSchema, sourcePath, targetPath, captureFormat) => {
|
|
57833
58724
|
await import_promises19.default.mkdir(import_path24.default.dirname(targetPath), { recursive: true });
|
|
57834
|
-
|
|
57835
|
-
if (captureFormat !== "JSONL" || internalFormat !== "CSV") {
|
|
58725
|
+
if (captureFormat !== "JSONL" || dsSchema.encoding !== "CSV") {
|
|
57836
58726
|
await import_promises19.default.copyFile(sourcePath, targetPath);
|
|
57837
58727
|
return;
|
|
57838
58728
|
}
|
|
57839
|
-
const
|
|
58729
|
+
const parseLine = LineParser_default.datasetParser(dsSchema);
|
|
57840
58730
|
const output = import_fs25.default.createWriteStream(targetPath);
|
|
57841
58731
|
try {
|
|
57842
58732
|
for await (const line of readLines(import_fs25.default.createReadStream(sourcePath))) {
|
|
57843
58733
|
if (!line.length) continue;
|
|
57844
|
-
const record =
|
|
58734
|
+
const record = parseLine(line);
|
|
57845
58735
|
if (!output.write(JSON.stringify(record) + "\n"))
|
|
57846
58736
|
await new Promise((resolve) => output.once("drain", resolve));
|
|
57847
58737
|
}
|
|
@@ -57854,7 +58744,7 @@ var init_ExecutorOrchestrator = __esm({
|
|
|
57854
58744
|
}
|
|
57855
58745
|
};
|
|
57856
58746
|
/**
|
|
57857
|
-
* Grabs the remote datasets and
|
|
58747
|
+
* Grabs the remote datasets and stages each one as a local `.extract` to work on
|
|
57858
58748
|
*/
|
|
57859
58749
|
this.readySourceFiles = async (args) => {
|
|
57860
58750
|
const { consumer, scope, consumerInputs, syntheticInputs, options: options2, onProgress } = args;
|
|
@@ -57864,7 +58754,7 @@ var init_ExecutorOrchestrator = __esm({
|
|
|
57864
58754
|
const reportProducer = (fraction) => onProgress?.((i6 + fraction) / consumer.producers.length);
|
|
57865
58755
|
const consumerInput = consumerInputs.get(cProd.name);
|
|
57866
58756
|
if (consumerInput) {
|
|
57867
|
-
const prod2 = ConsumerInputResolver_default.buildSyntheticProducer(
|
|
58757
|
+
const prod2 = ConsumerInputResolver_default.buildSyntheticProducer(consumerInput);
|
|
57868
58758
|
const response2 = { files: [{ fullUri: consumerInput.path, fileSize: consumerInput.fileSize }] };
|
|
57869
58759
|
Logger_default.log(`[${scope.id}] Producer "${cProd.name}" is a consumer input: ${consumerInput.fileSize} bytes (${i6 + 1}/${consumer.producers.length})`);
|
|
57870
58760
|
results.push({ prod: prod2, cProd, response: response2 });
|
|
@@ -58013,6 +58903,49 @@ var init_ExecutorOrchestrator = __esm({
|
|
|
58013
58903
|
}
|
|
58014
58904
|
return mainPath;
|
|
58015
58905
|
};
|
|
58906
|
+
/**
|
|
58907
|
+
* The passes that work across lines, run on the merged dataset. Returns the schema the last
|
|
58908
|
+
* pass to write the file left behind — its `rows` is the run's row count.
|
|
58909
|
+
*
|
|
58910
|
+
* `lineCount` goes FIRST: it walks each chunk's `outputCount` in order to find the worker a line
|
|
58911
|
+
* came from, which only holds while the file still has sum(outputCount) lines in worker order.
|
|
58912
|
+
*/
|
|
58913
|
+
this.runPostPasses = async (request) => {
|
|
58914
|
+
const { consumer, scope, executorResults, tracker, usageId } = request;
|
|
58915
|
+
const datasetPath = ExecutorScope_default.getMainPath(scope);
|
|
58916
|
+
const isMerged = executorResults.length > 1;
|
|
58917
|
+
let dsSchema = request.dsSchema;
|
|
58918
|
+
let counter = 0;
|
|
58919
|
+
if (isMerged && ConsumerManager_default.hasLineCountFields(consumer, true)) {
|
|
58920
|
+
Logger_default.log(`[${usageId}] Running unified lineCount pass across merged workers`);
|
|
58921
|
+
counter = performance.now();
|
|
58922
|
+
dsSchema = await ConsumerExecutor_default.processLineCount(consumer, dsSchema, datasetPath, executorResults);
|
|
58923
|
+
tracker.measure("process-line-count:main", performance.now() - counter);
|
|
58924
|
+
Logger_default.log(`[${usageId}] LineCount pass complete: normalized ${dsSchema.rows} rows in ${Math.round(performance.now() - counter)}ms`);
|
|
58925
|
+
}
|
|
58926
|
+
if (isMerged && consumer.options?.distinct === true) {
|
|
58927
|
+
Logger_default.log(`[${usageId}] Running unified distinct pass across merged workers`);
|
|
58928
|
+
counter = performance.now();
|
|
58929
|
+
dsSchema = await ConsumerExecutor_default.processDistinct(dsSchema, datasetPath);
|
|
58930
|
+
tracker.measure("process-distinct:main", performance.now() - counter);
|
|
58931
|
+
Logger_default.log(`[${usageId}] Distinct pass complete: ${dsSchema.rows} unique rows in ${Math.round(performance.now() - counter)}ms`);
|
|
58932
|
+
}
|
|
58933
|
+
if (isMerged && consumer.options?.distinctOn) {
|
|
58934
|
+
Logger_default.log(`[${usageId}] Running unified distinctOn pass (${consumer.options.distinctOn})`);
|
|
58935
|
+
counter = performance.now();
|
|
58936
|
+
dsSchema = await ConsumerExecutor_default.processDistinctOn(consumer, dsSchema, datasetPath);
|
|
58937
|
+
tracker.measure("process-distinct-on:main", performance.now() - counter);
|
|
58938
|
+
Logger_default.log(`[${usageId}] DistinctOn pass complete: ${dsSchema.rows} rows in ${Math.round(performance.now() - counter)}ms`);
|
|
58939
|
+
}
|
|
58940
|
+
if (consumer.options?.pivot) {
|
|
58941
|
+
Logger_default.log(`[${usageId}] Running pivot operation`);
|
|
58942
|
+
counter = performance.now();
|
|
58943
|
+
dsSchema = await ConsumerExecutor_default.processPivot(consumer, dsSchema, datasetPath);
|
|
58944
|
+
tracker.measure("process-pivot:main", performance.now() - counter);
|
|
58945
|
+
Logger_default.log(`[${usageId}] Pivot complete: ${dsSchema.rows} rows in ${Math.round(performance.now() - counter)}ms`);
|
|
58946
|
+
}
|
|
58947
|
+
return dsSchema;
|
|
58948
|
+
};
|
|
58016
58949
|
this.performCleanupOperations = async (scope, tracker) => {
|
|
58017
58950
|
const start = performance.now();
|
|
58018
58951
|
await ExecutorScope_default.clearScope(scope);
|
|
@@ -58034,6 +58967,7 @@ var init_ExecutorOrchestrator = __esm({
|
|
|
58034
58967
|
// ../../packages/executors/src/index.ts
|
|
58035
58968
|
init_src();
|
|
58036
58969
|
init_ConsumerExecutor();
|
|
58970
|
+
init_DatasetSchema();
|
|
58037
58971
|
|
|
58038
58972
|
// ../../packages/executors/src/Executor.ts
|
|
58039
58973
|
var import_fs22 = __toESM(require("fs"));
|
|
@@ -58189,6 +59123,7 @@ var Executor = class {
|
|
|
58189
59123
|
this.run = async (request) => {
|
|
58190
59124
|
Affirm_default(request, "Invalid request");
|
|
58191
59125
|
const { consumer, producer, prodDimensions, workerId, chunk, options: options2, scope, reportWork, lookups } = request;
|
|
59126
|
+
Affirm_default(request.dsSchema, `Worker ${workerId} was given no dataset schema to write with`);
|
|
58192
59127
|
const recordLimit = this.canBoundChunk(consumer) ? options2?.limit : null;
|
|
58193
59128
|
this.hydrateLookups(lookups);
|
|
58194
59129
|
const counter = performance.now();
|
|
@@ -58212,7 +59147,8 @@ var Executor = class {
|
|
|
58212
59147
|
const writer = this.openWriteStream(scope, workerId);
|
|
58213
59148
|
const fields = ConsumerManager_default.getExpandedFields(consumer);
|
|
58214
59149
|
const plan = RecordPlan_default.compile({ consumer, producer, fields, dimensions: prodDimensions });
|
|
58215
|
-
|
|
59150
|
+
let dsSchema = request.dsSchema;
|
|
59151
|
+
const outputPlan = OutputExecutor_default.compileOutput(consumer, dsSchema);
|
|
58216
59152
|
const linePlan = ProducerExecutor_default.compileLine(prodDimensions, chunk);
|
|
58217
59153
|
const recordSelector = ProducerManager_default.resolveRecordSelector(producer);
|
|
58218
59154
|
const { isFirstChunk, start, end } = chunk;
|
|
@@ -58296,14 +59232,16 @@ var Executor = class {
|
|
|
58296
59232
|
if (consumer.options?.distinct === true) {
|
|
58297
59233
|
Logger_default.log(`[${workerId}] Running distinct pass`);
|
|
58298
59234
|
perf = performance.now();
|
|
58299
|
-
|
|
59235
|
+
dsSchema = await ConsumerExecutor_default.processDistinct(dsSchema, result.resultUri);
|
|
59236
|
+
totalOutputCount = dsSchema.rows;
|
|
58300
59237
|
this._performance.measure("process-distinct", performance.now() - perf);
|
|
58301
59238
|
totalCycles++;
|
|
58302
59239
|
}
|
|
58303
59240
|
if (consumer.options?.distinctOn) {
|
|
58304
59241
|
Logger_default.log(`[${workerId}] Running distinctOn pass (${consumer.options.distinctOn})`);
|
|
58305
59242
|
perf = performance.now();
|
|
58306
|
-
|
|
59243
|
+
dsSchema = await ConsumerExecutor_default.processDistinctOn(consumer, dsSchema, result.resultUri);
|
|
59244
|
+
totalOutputCount = dsSchema.rows;
|
|
58307
59245
|
this._performance.measure("process-distinct-on", performance.now() - perf);
|
|
58308
59246
|
totalCycles++;
|
|
58309
59247
|
}
|
|
@@ -58353,6 +59291,7 @@ var Executor_default = Executor;
|
|
|
58353
59291
|
// ../../packages/executors/src/index.ts
|
|
58354
59292
|
init_ExecutorOrchestrator();
|
|
58355
59293
|
init_ExecutorPerformance();
|
|
59294
|
+
init_ExecutorWriter();
|
|
58356
59295
|
init_OrchestratorHelper();
|
|
58357
59296
|
|
|
58358
59297
|
// ../../packages/executors/src/workers/ExecutorWorkerRunner.ts
|
|
@@ -58361,6 +59300,8 @@ var import_dotenv = __toESM(require("dotenv"));
|
|
|
58361
59300
|
var import_worker_threads = require("worker_threads");
|
|
58362
59301
|
init_src();
|
|
58363
59302
|
init_src4();
|
|
59303
|
+
init_src10();
|
|
59304
|
+
init_DatasetSchema();
|
|
58364
59305
|
init_src2();
|
|
58365
59306
|
import_dotenv.default.configDotenv();
|
|
58366
59307
|
var MODULE_LOADED_AT = performance.timeOrigin + performance.now();
|
|
@@ -58411,6 +59352,7 @@ var runExecutorTask = async (workerData) => {
|
|
|
58411
59352
|
chunk,
|
|
58412
59353
|
options: options2,
|
|
58413
59354
|
scope,
|
|
59355
|
+
dsSchema: resolveDsSchema(consumer),
|
|
58414
59356
|
lookups,
|
|
58415
59357
|
reportWork: (packet) => import_workerpool2.default.workerEmit(packet)
|
|
58416
59358
|
});
|
|
@@ -58431,6 +59373,11 @@ var runExecutorTask = async (workerData) => {
|
|
|
58431
59373
|
throw error;
|
|
58432
59374
|
}
|
|
58433
59375
|
};
|
|
59376
|
+
var resolveDsSchema = (consumer) => {
|
|
59377
|
+
const sent = import_worker_threads.workerData?.dsSchema;
|
|
59378
|
+
if (sent) return sent;
|
|
59379
|
+
return DatasetSchema_default.compile(consumer, ConsumerManager_default.getExpandedFields(consumer));
|
|
59380
|
+
};
|
|
58434
59381
|
var registerExecutorWorker = () => {
|
|
58435
59382
|
import_workerpool2.default.worker({
|
|
58436
59383
|
executor: runExecutorTask
|
|
@@ -58507,8 +59454,7 @@ var SampleRunnerClass = class {
|
|
|
58507
59454
|
captureMainDatasetTo: capturePath,
|
|
58508
59455
|
captureFormat: "JSONL"
|
|
58509
59456
|
});
|
|
58510
|
-
const
|
|
58511
|
-
const records = captured.map((record) => this._inFieldOrder(consumer, record));
|
|
59457
|
+
const records = await this._readCapture(capturePath, size);
|
|
58512
59458
|
Logger_default.log(`[sample] Consumer "${consumer.name}": ${records.length} row(s) from ${result.inputCount} input line(s)`);
|
|
58513
59459
|
return {
|
|
58514
59460
|
kind: "consumer",
|
|
@@ -58556,28 +59502,6 @@ var SampleRunnerClass = class {
|
|
|
58556
59502
|
return this._inputProducers(nested, seen);
|
|
58557
59503
|
});
|
|
58558
59504
|
};
|
|
58559
|
-
/**
|
|
58560
|
-
* One row's keys in the order the consumer DECLARES its fields.
|
|
58561
|
-
*
|
|
58562
|
-
* A record comes out of a worker keyed in the order the producer's dimensions were read, which is
|
|
58563
|
-
* the source's layout and not the consumer's — so a consumer declaring country, plan, is_active
|
|
58564
|
-
* over a producer that stores is_active first would be shown its own columns in someone else's
|
|
58565
|
-
* order. Invisible in an exported JSON object; the whole shape of the table here.
|
|
58566
|
-
*
|
|
58567
|
-
* Anything the record carries that the field list does not name is appended rather than dropped:
|
|
58568
|
-
* a column that turns up unexpectedly is exactly the one worth seeing.
|
|
58569
|
-
*/
|
|
58570
|
-
this._inFieldOrder = (consumer, record) => {
|
|
58571
|
-
const declared = ConsumerManager_default.getExpandedFields(consumer).filter((field) => !field.cField.hidden).map((field) => field.finalKey);
|
|
58572
|
-
const ordered = {};
|
|
58573
|
-
for (const key of declared) {
|
|
58574
|
-
if (key in record) ordered[key] = record[key];
|
|
58575
|
-
}
|
|
58576
|
-
for (const key of Object.keys(record)) {
|
|
58577
|
-
if (!(key in ordered)) ordered[key] = record[key];
|
|
58578
|
-
}
|
|
58579
|
-
return ordered;
|
|
58580
|
-
};
|
|
58581
59505
|
/** The captured dataset, which `captureFormat: 'JSONL'` guarantees is one JSON object per line. */
|
|
58582
59506
|
this._readCapture = async (capturePath, size) => {
|
|
58583
59507
|
if (!import_fs26.default.existsSync(capturePath)) return [];
|