@forzalabs/remora 2.3.0 → 2.4.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 +46 -0
- package/documentation/README.md +1 -4
- package/index.js +776 -394
- package/json_schemas/consumer-schema.json +9 -10
- package/json_schemas/producer-schema.json +0 -38
- package/package.json +4 -2
- package/workers/ExecutorWorker.js +788 -389
|
@@ -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 fs32 = 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
|
+
fs32.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
|
+
fs32.close(fd, noop);
|
|
8575
8575
|
return;
|
|
8576
8576
|
}
|
|
8577
|
-
return
|
|
8577
|
+
return fs32.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 fs32 = 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 = fs32.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
|
+
fs32.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 = fs32.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
|
+
fs32.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
|
+
fs32.exists(tmppath, (exists) => {
|
|
9201
9201
|
if (!exists) {
|
|
9202
9202
|
return cb(null);
|
|
9203
9203
|
}
|
|
9204
9204
|
fileName = `${basename}${i6}${ext}${isZipped}`;
|
|
9205
|
-
|
|
9205
|
+
fs32.rename(tmppath, path22.join(this.dirname, fileName), cb);
|
|
9206
9206
|
});
|
|
9207
9207
|
}.bind(this, x2));
|
|
9208
9208
|
}
|
|
9209
9209
|
asyncSeries(tasks, () => {
|
|
9210
|
-
|
|
9210
|
+
fs32.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
|
+
fs32.access(src, fs32.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 = fs32.createReadStream(src);
|
|
9232
|
+
var out = fs32.createWriteStream(dest);
|
|
9233
9233
|
out.on("finish", () => {
|
|
9234
|
-
|
|
9234
|
+
fs32.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 (!fs32.existsSync(dirPath)) {
|
|
9241
|
+
fs32.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.4.0",
|
|
11813
11813
|
backendVersion: 1,
|
|
11814
11814
|
backendPort: 5088,
|
|
11815
11815
|
workerVersion: 2,
|
|
@@ -11938,7 +11938,25 @@ var init_Constants = __esm({
|
|
|
11938
11938
|
* Rows a sample may ask for out of the pipeline. A sample is a look at the data, never an
|
|
11939
11939
|
* export of it — `remora sample -s` and the preview's Data tab are both held to this.
|
|
11940
11940
|
*/
|
|
11941
|
-
MAX_SAMPLE_ROWS: 100
|
|
11941
|
+
MAX_SAMPLE_ROWS: 100,
|
|
11942
|
+
/**
|
|
11943
|
+
* Values held in memory while one parquet row group is encoded. A parquet writer buffers a
|
|
11944
|
+
* whole row group before it can compress and flush it, so this — not the dataset size — is
|
|
11945
|
+
* what bounds the export's peak memory. Budgeted in CELLS rather than rows because the two
|
|
11946
|
+
* differ by three orders of magnitude across consumers: 100,000 rows of a 4-column consumer
|
|
11947
|
+
* is 400k values, of a 1,300-field one it is 130 million.
|
|
11948
|
+
*/
|
|
11949
|
+
PARQUET_ROW_GROUP_CELLS: 1e6,
|
|
11950
|
+
/**
|
|
11951
|
+
* Floor on the derived row count, so a consumer with hundreds of columns still writes row
|
|
11952
|
+
* groups big enough to compress and to read back without a seek per handful of rows.
|
|
11953
|
+
*/
|
|
11954
|
+
PARQUET_MIN_ROW_GROUP_ROWS: 1e3,
|
|
11955
|
+
/**
|
|
11956
|
+
* Ceiling on the derived row count, so a one-column consumer does not buffer a million rows
|
|
11957
|
+
* to save a few row-group footers.
|
|
11958
|
+
*/
|
|
11959
|
+
PARQUET_MAX_ROW_GROUP_ROWS: 1e5
|
|
11942
11960
|
}
|
|
11943
11961
|
};
|
|
11944
11962
|
Constants_default = CONSTANTS;
|
|
@@ -12032,11 +12050,26 @@ var init_ExecutorScope = __esm({
|
|
|
12032
12050
|
import_path2 = __toESM(require("path"), 1);
|
|
12033
12051
|
import_fs2 = __toESM(require("fs"), 1);
|
|
12034
12052
|
import_promises = __toESM(require("fs/promises"), 1);
|
|
12053
|
+
init_src();
|
|
12035
12054
|
init_src3();
|
|
12036
12055
|
ExecutorScopeClass = class {
|
|
12037
12056
|
constructor() {
|
|
12038
12057
|
this.WORKERS_FOLDER = "workers";
|
|
12039
12058
|
this.PRODUCERS_FOLDER = "producers";
|
|
12059
|
+
/**
|
|
12060
|
+
* The scope folders THIS process opened and has not cleared. The only list `deepClear` may
|
|
12061
|
+
* delete from: two remora processes share one `.temp`, so a listing cannot tell a crashed run's
|
|
12062
|
+
* leftovers from another process's live run.
|
|
12063
|
+
*/
|
|
12064
|
+
this._ownedScopes = /* @__PURE__ */ new Set();
|
|
12065
|
+
/** Registers a scope as this process's, so a crash on the way out clears it. */
|
|
12066
|
+
this.open = (scope) => {
|
|
12067
|
+
Affirm_default(scope?.folder, "Invalid executor scope");
|
|
12068
|
+
this._ownedScopes.add(scope.folder);
|
|
12069
|
+
return scope;
|
|
12070
|
+
};
|
|
12071
|
+
/** The scopes this process still holds. Empty once every run it started has cleaned up. */
|
|
12072
|
+
this.getOwnedScopes = () => [...this._ownedScopes];
|
|
12040
12073
|
this.getBasePath = () => import_path2.default.join(Constants_default.defaults.REMORA_PATH, Constants_default.defaults.PRODUCER_TEMP_FOLDER);
|
|
12041
12074
|
this.getWorkerPath = (scope, workerId) => {
|
|
12042
12075
|
return import_path2.default.join(
|
|
@@ -12047,6 +12080,13 @@ var init_ExecutorScope = __esm({
|
|
|
12047
12080
|
`${workerId}.dataset`
|
|
12048
12081
|
);
|
|
12049
12082
|
};
|
|
12083
|
+
/**
|
|
12084
|
+
* Where a producer's source is staged before any worker reads it: a CSV extract WITH a header,
|
|
12085
|
+
* written by whichever driver read the source.
|
|
12086
|
+
*
|
|
12087
|
+
* `.extract`, not `.dataset`: the two are different formats, and one extension meaning both is
|
|
12088
|
+
* how a reader ends up applying the internal positional rules to a file that follows none.
|
|
12089
|
+
*/
|
|
12050
12090
|
this.getProducerPath = (scope, producer, sourceFileKey) => {
|
|
12051
12091
|
return import_path2.default.join(
|
|
12052
12092
|
this.getBasePath(),
|
|
@@ -12054,7 +12094,7 @@ var init_ExecutorScope = __esm({
|
|
|
12054
12094
|
scope.folder,
|
|
12055
12095
|
this.PRODUCERS_FOLDER,
|
|
12056
12096
|
producer.name,
|
|
12057
|
-
`${sourceFileKey}.
|
|
12097
|
+
`${sourceFileKey}.extract`
|
|
12058
12098
|
);
|
|
12059
12099
|
};
|
|
12060
12100
|
this.getMainPath = (scope) => {
|
|
@@ -12090,16 +12130,32 @@ var init_ExecutorScope = __esm({
|
|
|
12090
12130
|
if (import_fs2.default.existsSync(scopePath)) {
|
|
12091
12131
|
await import_promises.default.rm(scopePath, { recursive: true, force: true });
|
|
12092
12132
|
}
|
|
12133
|
+
this._ownedScopes.delete(scope.folder);
|
|
12093
12134
|
};
|
|
12135
|
+
/**
|
|
12136
|
+
* Clears what this process left open, on the way out. Synchronous: its one caller is the `exit`
|
|
12137
|
+
* handler. A run that finished cleared its own scope, so a clean exit finds nothing to do.
|
|
12138
|
+
*/
|
|
12094
12139
|
this.deepClear = () => {
|
|
12095
12140
|
const basePath = this.getBasePath();
|
|
12096
|
-
const
|
|
12097
|
-
for (const scopeFolder of openScopes) {
|
|
12141
|
+
for (const scopeFolder of this._ownedScopes) {
|
|
12098
12142
|
const scopePath = import_path2.default.join(basePath, scopeFolder);
|
|
12099
12143
|
if (import_fs2.default.existsSync(scopePath)) {
|
|
12100
12144
|
import_fs2.default.rmSync(scopePath, { recursive: true, force: true });
|
|
12101
12145
|
}
|
|
12102
12146
|
}
|
|
12147
|
+
this._ownedScopes.clear();
|
|
12148
|
+
};
|
|
12149
|
+
/**
|
|
12150
|
+
* Every scope folder on disk, whoever opened it. Answers "what is in .temp?", not "what is mine
|
|
12151
|
+
* to delete" — nothing may be removed on the strength of it (see `getOwnedScopes`). The parallel
|
|
12152
|
+
* canary samples it to watch concurrent runs come and go.
|
|
12153
|
+
*/
|
|
12154
|
+
this.getOpenScopes = () => {
|
|
12155
|
+
const basePath = this.getBasePath();
|
|
12156
|
+
if (!import_fs2.default.existsSync(basePath))
|
|
12157
|
+
return [];
|
|
12158
|
+
return import_fs2.default.readdirSync(basePath, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name).filter((folder) => folder !== "logs" && folder !== "usage");
|
|
12103
12159
|
};
|
|
12104
12160
|
this.ensurePath = (fileUri) => {
|
|
12105
12161
|
const dir = import_path2.default.dirname(fileUri);
|
|
@@ -12108,12 +12164,6 @@ var init_ExecutorScope = __esm({
|
|
|
12108
12164
|
if (!import_fs2.default.existsSync(fileUri))
|
|
12109
12165
|
import_fs2.default.writeFileSync(fileUri, "");
|
|
12110
12166
|
};
|
|
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
12167
|
}
|
|
12118
12168
|
};
|
|
12119
12169
|
ExecutorScope = new ExecutorScopeClass();
|
|
@@ -12167,8 +12217,7 @@ var init_ProcessShutdownManager = __esm({
|
|
|
12167
12217
|
if (this._cleaned)
|
|
12168
12218
|
return;
|
|
12169
12219
|
this._cleaned = true;
|
|
12170
|
-
const
|
|
12171
|
-
const scopeCount = openScopes.length;
|
|
12220
|
+
const scopeCount = ExecutorScope_default.getOwnedScopes().length;
|
|
12172
12221
|
const shutdownState = this._shutdownState;
|
|
12173
12222
|
const shutdownDescription = `${shutdownState?.type ?? "intentional"} shutdown (${shutdownState?.reason ?? `exit:${code}`})`;
|
|
12174
12223
|
if (scopeCount === 0) {
|
|
@@ -12579,7 +12628,7 @@ var init_ResourcesUtils = __esm({
|
|
|
12579
12628
|
Affirm_default(subConsumer, `No producer found with name "${cProd.name}"`);
|
|
12580
12629
|
return this.getConsumerOutputColumns(subConsumer);
|
|
12581
12630
|
} else {
|
|
12582
|
-
|
|
12631
|
+
return producer.dimensions.map((x2) => ({
|
|
12583
12632
|
consumerAlias: null,
|
|
12584
12633
|
consumerKey: null,
|
|
12585
12634
|
nameInProducer: x2.name,
|
|
@@ -12587,15 +12636,6 @@ var init_ResourcesUtils = __esm({
|
|
|
12587
12636
|
dimension: x2,
|
|
12588
12637
|
owner: cProd.name
|
|
12589
12638
|
}));
|
|
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
12639
|
}
|
|
12600
12640
|
});
|
|
12601
12641
|
};
|
|
@@ -12655,7 +12695,6 @@ var init_ResourcesUtils = __esm({
|
|
|
12655
12695
|
nameInProducer: col.nameInProducer,
|
|
12656
12696
|
aliasInProducer: col.nameInProducer,
|
|
12657
12697
|
dimension: col.dimension,
|
|
12658
|
-
measure: col.measure,
|
|
12659
12698
|
owner: consumer.name
|
|
12660
12699
|
});
|
|
12661
12700
|
}
|
|
@@ -12669,7 +12708,6 @@ var init_ResourcesUtils = __esm({
|
|
|
12669
12708
|
nameInProducer: match2.name,
|
|
12670
12709
|
aliasInProducer: match2.name,
|
|
12671
12710
|
dimension: match2.column.dimension,
|
|
12672
|
-
measure: match2.column.measure,
|
|
12673
12711
|
owner: consumer.name
|
|
12674
12712
|
});
|
|
12675
12713
|
}
|
|
@@ -12683,7 +12721,6 @@ var init_ResourcesUtils = __esm({
|
|
|
12683
12721
|
nameInProducer: finalKey,
|
|
12684
12722
|
aliasInProducer: finalKey,
|
|
12685
12723
|
dimension: match?.dimension ?? { name: finalKey, type: "string" },
|
|
12686
|
-
measure: match?.measure,
|
|
12687
12724
|
owner: consumer.name
|
|
12688
12725
|
});
|
|
12689
12726
|
}
|
|
@@ -13135,11 +13172,8 @@ var init_Validator = __esm({
|
|
|
13135
13172
|
return errors2;
|
|
13136
13173
|
};
|
|
13137
13174
|
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
|
-
}
|
|
13175
|
+
if ((consumer.outputs ?? []).length > 1)
|
|
13176
|
+
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
13177
|
for (const output of consumer.outputs) {
|
|
13144
13178
|
const format4 = output.format.toUpperCase();
|
|
13145
13179
|
if (format4 === "CSV" || format4 === "JSON" || format4 === "PARQUET" || format4 === "TXT") {
|
|
@@ -13237,47 +13271,41 @@ var init_Validator = __esm({
|
|
|
13237
13271
|
const missingKeys = fieldKeys.includes("*") ? [] : keys.filter((key) => !fieldKeys.includes(key));
|
|
13238
13272
|
if (missingKeys.length > 0)
|
|
13239
13273
|
errors.push(`"split.by" references key(s) "${missingKeys.join(", ")}" that are not present in the consumer "${consumer.name}".`);
|
|
13240
|
-
const
|
|
13241
|
-
if (
|
|
13274
|
+
const format4 = consumer.outputs?.[0]?.format.toUpperCase();
|
|
13275
|
+
if (!positionalFormats.includes(format4)) return errors;
|
|
13242
13276
|
const hiddenKeys = keys.filter((key) => (consumer.fields ?? []).some((x2) => (x2.alias ?? x2.key) === key && x2.hidden));
|
|
13243
13277
|
if (hiddenKeys.length > 0)
|
|
13244
|
-
errors.push(`"split.by" of consumer "${consumer.name}" references hidden field(s) "${hiddenKeys.join(", ")}", which the ${
|
|
13278
|
+
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
13279
|
return errors;
|
|
13246
13280
|
};
|
|
13247
13281
|
/**
|
|
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.
|
|
13282
|
+
* Checks `omitNull`, which drops a record's null fields on export. It only means anything where
|
|
13283
|
+
* a record carries its own field names: leaving a positional column out shifts every column
|
|
13284
|
+
* after it, so a positional output declaring it is rejected rather than ignored.
|
|
13252
13285
|
*/
|
|
13253
13286
|
this.validateOmitNull = (consumer) => {
|
|
13254
13287
|
const errors = [];
|
|
13255
13288
|
const positionalFormats = ["CSV", "TXT", "PARQUET"];
|
|
13256
13289
|
for (const output of (consumer.outputs ?? []).filter((x2) => x2.omitNull === true)) {
|
|
13257
13290
|
const format4 = output.format.toUpperCase();
|
|
13258
|
-
if (positionalFormats.includes(format4))
|
|
13291
|
+
if (positionalFormats.includes(format4))
|
|
13259
13292
|
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
13293
|
}
|
|
13266
13294
|
return errors;
|
|
13267
13295
|
};
|
|
13268
13296
|
/**
|
|
13269
13297
|
* 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.
|
|
13298
|
+
* record carries its own field names, a parquet file carries its own schema, and an API response
|
|
13299
|
+
* is not a file, so the flag means nothing there — rejected rather than ignored, so a config that
|
|
13300
|
+
* asks for a header always gets one.
|
|
13273
13301
|
*/
|
|
13274
13302
|
this.validateHeader = (consumer) => {
|
|
13275
13303
|
const errors = [];
|
|
13276
|
-
const
|
|
13304
|
+
const headerFormats = ["CSV", "TXT"];
|
|
13277
13305
|
for (const output of (consumer.outputs ?? []).filter((x2) => Algo_default.hasVal(x2.header))) {
|
|
13278
13306
|
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: a ${format4} record already carries its own field names.`);
|
|
13307
|
+
if (!headerFormats.includes(format4))
|
|
13308
|
+
errors.push(`The ${format4} output of consumer "${consumer.name}" declares "header", which only applies to CSV/TXT output: a ${format4} ${format4 === "PARQUET" ? "file carries its own schema" : "record already carries its own field names"}.`);
|
|
13281
13309
|
}
|
|
13282
13310
|
return errors;
|
|
13283
13311
|
};
|
|
@@ -33669,6 +33697,96 @@ var init_CSVParser = __esm({
|
|
|
33669
33697
|
}
|
|
33670
33698
|
});
|
|
33671
33699
|
|
|
33700
|
+
// ../../packages/parsing/src/DatasetCodec.ts
|
|
33701
|
+
var NULL_FIELD, NEEDS_ESCAPE, DatasetCodecClass, DatasetCodec, DatasetCodec_default;
|
|
33702
|
+
var init_DatasetCodec = __esm({
|
|
33703
|
+
"../../packages/parsing/src/DatasetCodec.ts"() {
|
|
33704
|
+
init_src();
|
|
33705
|
+
NULL_FIELD = "";
|
|
33706
|
+
NEEDS_ESCAPE = /["\n\r]/;
|
|
33707
|
+
DatasetCodecClass = class {
|
|
33708
|
+
constructor() {
|
|
33709
|
+
/**
|
|
33710
|
+
* One value as an internal positional field: quoted, with `"`, `\n` and `\r` escaped - or the
|
|
33711
|
+
* unquoted null marker when there is no value.
|
|
33712
|
+
*/
|
|
33713
|
+
this.encodeField = (value) => {
|
|
33714
|
+
if (value === null || value === void 0) return NULL_FIELD;
|
|
33715
|
+
const text = typeof value === "string" ? value : String(value);
|
|
33716
|
+
if (!NEEDS_ESCAPE.test(text)) return `"${text}"`;
|
|
33717
|
+
return `"${text.replaceAll('"', '""').replaceAll("\r", "\\r").replaceAll("\n", "\\n")}"`;
|
|
33718
|
+
};
|
|
33719
|
+
/** One record as an internal positional line. The values are already in the schema's order. */
|
|
33720
|
+
this.encodeRow = (values, delimiter = ",") => {
|
|
33721
|
+
Affirm_default(values, "Invalid values");
|
|
33722
|
+
Affirm_default(delimiter, "Invalid delimiter");
|
|
33723
|
+
let row = "";
|
|
33724
|
+
for (let index = 0; index < values.length; index++) {
|
|
33725
|
+
if (index > 0)
|
|
33726
|
+
row += delimiter;
|
|
33727
|
+
row += this.encodeField(values[index]);
|
|
33728
|
+
}
|
|
33729
|
+
return row;
|
|
33730
|
+
};
|
|
33731
|
+
/**
|
|
33732
|
+
* One internal positional line split back into its raw fields, with a null still a null. The
|
|
33733
|
+
* array may be SHORTER than the schema's column list; a reader treats a missing entry as null.
|
|
33734
|
+
*/
|
|
33735
|
+
this.decodeRow = (line, delimiter = ",") => {
|
|
33736
|
+
Affirm_default(delimiter, "Invalid delimiter");
|
|
33737
|
+
const fields = [];
|
|
33738
|
+
const length = line?.length ?? 0;
|
|
33739
|
+
const delimiterCode = delimiter.charCodeAt(0);
|
|
33740
|
+
let fieldStart = 0;
|
|
33741
|
+
let fieldEnd = 0;
|
|
33742
|
+
let inQuotes = false;
|
|
33743
|
+
let hasQuotes = false;
|
|
33744
|
+
let index = 0;
|
|
33745
|
+
while (index < length) {
|
|
33746
|
+
const char = line.charCodeAt(index);
|
|
33747
|
+
if (char === 34) {
|
|
33748
|
+
if (!inQuotes) {
|
|
33749
|
+
inQuotes = true;
|
|
33750
|
+
hasQuotes = true;
|
|
33751
|
+
fieldStart = index + 1;
|
|
33752
|
+
} else if (line.charCodeAt(index + 1) === 34) {
|
|
33753
|
+
index++;
|
|
33754
|
+
} else {
|
|
33755
|
+
inQuotes = false;
|
|
33756
|
+
fieldEnd = index;
|
|
33757
|
+
}
|
|
33758
|
+
} else if (char === delimiterCode && !inQuotes) {
|
|
33759
|
+
fields.push(this._field(line, fieldStart, fieldEnd, hasQuotes));
|
|
33760
|
+
fieldStart = index + 1;
|
|
33761
|
+
fieldEnd = 0;
|
|
33762
|
+
hasQuotes = false;
|
|
33763
|
+
}
|
|
33764
|
+
index++;
|
|
33765
|
+
}
|
|
33766
|
+
fields.push(this._field(line, fieldStart, fieldEnd, hasQuotes));
|
|
33767
|
+
return fields;
|
|
33768
|
+
};
|
|
33769
|
+
/**
|
|
33770
|
+
* Re-encodes an internal line with another delimiter, which is what a `TXT` export is. Not via
|
|
33771
|
+
* `CSVParser`: a plain parse reads both the null marker and `""` as the empty string.
|
|
33772
|
+
*/
|
|
33773
|
+
this.redelimit = (line, delimiter) => {
|
|
33774
|
+
Affirm_default(delimiter, "Invalid delimiter");
|
|
33775
|
+
return this.encodeRow(this.decodeRow(line), delimiter);
|
|
33776
|
+
};
|
|
33777
|
+
/** One field's text, or null for the unquoted marker. Only a quoted token carries a value. */
|
|
33778
|
+
this._field = (line, start, end, hasQuotes) => {
|
|
33779
|
+
if (!hasQuotes) return null;
|
|
33780
|
+
const raw = line.slice(start, end);
|
|
33781
|
+
return raw.indexOf('"') === -1 ? raw : raw.replaceAll('""', '"');
|
|
33782
|
+
};
|
|
33783
|
+
}
|
|
33784
|
+
};
|
|
33785
|
+
DatasetCodec = new DatasetCodecClass();
|
|
33786
|
+
DatasetCodec_default = DatasetCodec;
|
|
33787
|
+
}
|
|
33788
|
+
});
|
|
33789
|
+
|
|
33672
33790
|
// ../../packages/parsing/src/FixedWidthParser.ts
|
|
33673
33791
|
var FixedWidthParserClass, FixedWidthParser, FixedWidthParser_default;
|
|
33674
33792
|
var init_FixedWidthParser = __esm({
|
|
@@ -33881,7 +33999,7 @@ var require_pend = __commonJS({
|
|
|
33881
33999
|
// ../../node_modules/yauzl/fd-slicer.js
|
|
33882
34000
|
var require_fd_slicer = __commonJS({
|
|
33883
34001
|
"../../node_modules/yauzl/fd-slicer.js"(exports2) {
|
|
33884
|
-
var
|
|
34002
|
+
var fs32 = require("fs");
|
|
33885
34003
|
var util = require("util");
|
|
33886
34004
|
var stream = require("stream");
|
|
33887
34005
|
var Readable10 = stream.Readable;
|
|
@@ -33901,7 +34019,7 @@ var require_fd_slicer = __commonJS({
|
|
|
33901
34019
|
FdSlicer.prototype.read = function(buffer, offset, length, position, callback) {
|
|
33902
34020
|
var self2 = this;
|
|
33903
34021
|
self2.pend.go(function(cb) {
|
|
33904
|
-
|
|
34022
|
+
fs32.read(self2.fd, buffer, offset, length, position, function(err2, bytesRead, buffer2) {
|
|
33905
34023
|
cb();
|
|
33906
34024
|
callback(err2, bytesRead, buffer2);
|
|
33907
34025
|
});
|
|
@@ -33918,7 +34036,7 @@ var require_fd_slicer = __commonJS({
|
|
|
33918
34036
|
self2.refCount -= 1;
|
|
33919
34037
|
if (self2.refCount < 0) throw new Error("invalid unref");
|
|
33920
34038
|
if (self2.refCount > 0) return;
|
|
33921
|
-
|
|
34039
|
+
fs32.close(self2.fd, onCloseDone);
|
|
33922
34040
|
function onCloseDone(err2) {
|
|
33923
34041
|
if (err2) {
|
|
33924
34042
|
self2.emit("error", err2);
|
|
@@ -33950,7 +34068,7 @@ var require_fd_slicer = __commonJS({
|
|
|
33950
34068
|
}
|
|
33951
34069
|
self2.context.pend.go(function(cb) {
|
|
33952
34070
|
var buffer = Buffer.allocUnsafe(toRead);
|
|
33953
|
-
|
|
34071
|
+
fs32.read(self2.context.fd, buffer, 0, toRead, self2.pos, function(err2, bytesRead) {
|
|
33954
34072
|
if (err2) {
|
|
33955
34073
|
self2.destroy(err2);
|
|
33956
34074
|
} else if (bytesRead === 0) {
|
|
@@ -34310,7 +34428,7 @@ var require_crc32 = __commonJS({
|
|
|
34310
34428
|
// ../../node_modules/yauzl/index.js
|
|
34311
34429
|
var require_yauzl = __commonJS({
|
|
34312
34430
|
"../../node_modules/yauzl/index.js"(exports2) {
|
|
34313
|
-
var
|
|
34431
|
+
var fs32 = require("fs");
|
|
34314
34432
|
var zlib3 = require("zlib");
|
|
34315
34433
|
var fd_slicer = require_fd_slicer();
|
|
34316
34434
|
var util = require("util");
|
|
@@ -34379,10 +34497,10 @@ var require_yauzl = __commonJS({
|
|
|
34379
34497
|
if (options2.validateEntrySizes == null) options2.validateEntrySizes = true;
|
|
34380
34498
|
if (options2.strictFileNames == null) options2.strictFileNames = false;
|
|
34381
34499
|
if (callback == null) callback = defaultCallback;
|
|
34382
|
-
|
|
34500
|
+
fs32.open(path22, "r", function(err2, fd) {
|
|
34383
34501
|
if (err2) return callback(err2);
|
|
34384
34502
|
fromFd(fd, options2, function(err3, zipfile) {
|
|
34385
|
-
if (err3)
|
|
34503
|
+
if (err3) fs32.close(fd, defaultCallback);
|
|
34386
34504
|
callback(err3, zipfile);
|
|
34387
34505
|
});
|
|
34388
34506
|
});
|
|
@@ -34399,7 +34517,7 @@ var require_yauzl = __commonJS({
|
|
|
34399
34517
|
if (options2.validateEntrySizes == null) options2.validateEntrySizes = true;
|
|
34400
34518
|
if (options2.strictFileNames == null) options2.strictFileNames = false;
|
|
34401
34519
|
if (callback == null) callback = defaultCallback;
|
|
34402
|
-
|
|
34520
|
+
fs32.fstat(fd, function(err2, stats) {
|
|
34403
34521
|
if (err2) return callback(err2);
|
|
34404
34522
|
var reader = new fd_slicer.FdSlicer(fd);
|
|
34405
34523
|
fromRandomAccessReader(reader, stats.size, options2, callback);
|
|
@@ -38478,6 +38596,7 @@ var init_XMLParser = __esm({
|
|
|
38478
38596
|
var init_src5 = __esm({
|
|
38479
38597
|
"../../packages/parsing/src/index.ts"() {
|
|
38480
38598
|
init_CSVParser();
|
|
38599
|
+
init_DatasetCodec();
|
|
38481
38600
|
init_FixedWidthParser();
|
|
38482
38601
|
init_ParseCompression();
|
|
38483
38602
|
init_ParseHelper();
|
|
@@ -47526,7 +47645,7 @@ var init_ProducerManager = __esm({
|
|
|
47526
47645
|
});
|
|
47527
47646
|
|
|
47528
47647
|
// ../../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;
|
|
47648
|
+
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
47649
|
var init_TypeCaster = __esm({
|
|
47531
47650
|
"../../packages/engines/src/transform/TypeCaster.ts"() {
|
|
47532
47651
|
init_src();
|
|
@@ -47547,6 +47666,7 @@ var init_TypeCaster = __esm({
|
|
|
47547
47666
|
{ token: "mm", width: 2 },
|
|
47548
47667
|
{ token: "ss", width: 2 }
|
|
47549
47668
|
];
|
|
47669
|
+
TRUE_VALUES = /^(1|true|yes|y|t)$/i;
|
|
47550
47670
|
TYPE_DEFAULTS = {
|
|
47551
47671
|
string: "",
|
|
47552
47672
|
number: 0,
|
|
@@ -47556,7 +47676,8 @@ var init_TypeCaster = __esm({
|
|
|
47556
47676
|
TypeCasterClass = class {
|
|
47557
47677
|
constructor() {
|
|
47558
47678
|
/**
|
|
47559
|
-
* Casts a value for a DECLARED
|
|
47679
|
+
* Casts a value for a DECLARED field: the type cast, plus what a MISSING value means for it. The
|
|
47680
|
+
* field is a producer dimension on the read path and a dataset column on the re-read path.
|
|
47560
47681
|
*
|
|
47561
47682
|
* Missing is null/undefined, and — for every type but `string` — the empty string, which is how
|
|
47562
47683
|
* a NULL arrives from every source that stages through CSV (SQL, Delta Share, CSV files) and is
|
|
@@ -47588,6 +47709,7 @@ var init_TypeCaster = __esm({
|
|
|
47588
47709
|
switch (type) {
|
|
47589
47710
|
case "boolean": {
|
|
47590
47711
|
if (typeof value === "boolean") return value;
|
|
47712
|
+
if (typeof value === "string") return TRUE_VALUES.test(value.trim());
|
|
47591
47713
|
return Boolean(value);
|
|
47592
47714
|
}
|
|
47593
47715
|
case "datetime":
|
|
@@ -47726,6 +47848,7 @@ var init_TypeCaster = __esm({
|
|
|
47726
47848
|
var LineParserClass, LineParser, LineParser_default;
|
|
47727
47849
|
var init_LineParser = __esm({
|
|
47728
47850
|
"../../packages/engines/src/parsing/LineParser.ts"() {
|
|
47851
|
+
init_src();
|
|
47729
47852
|
init_TypeCaster();
|
|
47730
47853
|
init_src5();
|
|
47731
47854
|
LineParserClass = class {
|
|
@@ -47818,22 +47941,23 @@ var init_LineParser = __esm({
|
|
|
47818
47941
|
return record;
|
|
47819
47942
|
};
|
|
47820
47943
|
/**
|
|
47821
|
-
*
|
|
47822
|
-
|
|
47823
|
-
|
|
47824
|
-
const parts = CSVParser_default.parseRow(line, delimiter ?? ",");
|
|
47825
|
-
const record = {};
|
|
47826
|
-
for (const [index, field] of fields.entries()) {
|
|
47827
|
-
const fieldKey = field.finalKey;
|
|
47828
|
-
record[fieldKey] = field.dimension ? TypeCaster_default.castDimension(parts[index], field.dimension) : TypeCaster_default.cast(parts[index], "string");
|
|
47829
|
-
}
|
|
47830
|
-
return record;
|
|
47831
|
-
};
|
|
47832
|
-
/**
|
|
47833
|
-
* Used ONLY to parse internal records (inside the .dataset) since I know they are already prepared in the correct way
|
|
47944
|
+
* The parser of one staged `.dataset`, compiled once from the schema that file was written with.
|
|
47945
|
+
* The schema is the ONLY description of the file - it has no header - and there is deliberately
|
|
47946
|
+
* no overload taking a consumer's field list, which cannot know about a shape a pass changed.
|
|
47834
47947
|
*/
|
|
47835
|
-
this.
|
|
47836
|
-
|
|
47948
|
+
this.datasetParser = (dsSchema) => {
|
|
47949
|
+
Affirm_default(dsSchema, "Invalid dataset schema");
|
|
47950
|
+
Affirm_default.hasItems(dsSchema.columns, "The dataset schema declares no column to read");
|
|
47951
|
+
if (dsSchema.encoding === "JSON")
|
|
47952
|
+
return (line) => JSON.parse(line);
|
|
47953
|
+
const { columns } = dsSchema;
|
|
47954
|
+
return (line) => {
|
|
47955
|
+
const parts = DatasetCodec_default.decodeRow(line);
|
|
47956
|
+
const record = {};
|
|
47957
|
+
for (let index = 0; index < columns.length; index++)
|
|
47958
|
+
record[columns[index].name] = TypeCaster_default.castDimension(parts[index], columns[index]);
|
|
47959
|
+
return record;
|
|
47960
|
+
};
|
|
47837
47961
|
};
|
|
47838
47962
|
}
|
|
47839
47963
|
};
|
|
@@ -47950,7 +48074,7 @@ var init_ProducerEngine = __esm({
|
|
|
47950
48074
|
let effectiveProducer = producer;
|
|
47951
48075
|
let effectiveSource = source;
|
|
47952
48076
|
if (compressionType) {
|
|
47953
|
-
scope = { id: (0, import_crypto6.randomUUID)(), folder: `sample-${(0, import_crypto6.randomUUID)()}`, workersId: [] };
|
|
48077
|
+
scope = ExecutorScope_default.open({ id: (0, import_crypto6.randomUUID)(), folder: `sample-${(0, import_crypto6.randomUUID)()}`, workersId: [] });
|
|
47954
48078
|
const driver = await DriverFactory_default.instantiateSource(source);
|
|
47955
48079
|
const readyResult = await driver.ready({ producer, scope });
|
|
47956
48080
|
const firstFile = readyResult.files[0].fullUri;
|
|
@@ -47960,7 +48084,7 @@ var init_ProducerEngine = __esm({
|
|
|
47960
48084
|
...producer,
|
|
47961
48085
|
settings: {
|
|
47962
48086
|
...producer.settings,
|
|
47963
|
-
fileKey: readyResult.files.length > 1 ? "%.
|
|
48087
|
+
fileKey: readyResult.files.length > 1 ? "%.extract" : import_path14.default.basename(firstFile),
|
|
47964
48088
|
compressionType: void 0
|
|
47965
48089
|
}
|
|
47966
48090
|
};
|
|
@@ -51213,8 +51337,7 @@ var init_ConsumerManager = __esm({
|
|
|
51213
51337
|
from: x2.owner
|
|
51214
51338
|
},
|
|
51215
51339
|
finalKey: x2.nameInProducer,
|
|
51216
|
-
dimension: x2.dimension
|
|
51217
|
-
measure: x2.measure
|
|
51340
|
+
dimension: x2.dimension
|
|
51218
51341
|
})));
|
|
51219
51342
|
} else {
|
|
51220
51343
|
const from = field.cField.from ?? (consumer.producers.length === 1 ? consumer.producers[0].name : null);
|
|
@@ -51226,8 +51349,7 @@ var init_ConsumerManager = __esm({
|
|
|
51226
51349
|
from: col.owner
|
|
51227
51350
|
},
|
|
51228
51351
|
finalKey: col.nameInProducer,
|
|
51229
|
-
dimension: col.dimension
|
|
51230
|
-
measure: col.measure
|
|
51352
|
+
dimension: col.dimension
|
|
51231
51353
|
});
|
|
51232
51354
|
});
|
|
51233
51355
|
}
|
|
@@ -51242,13 +51364,12 @@ var init_ConsumerManager = __esm({
|
|
|
51242
51364
|
from: field.cField.from ?? match.column.owner
|
|
51243
51365
|
},
|
|
51244
51366
|
finalKey: match.name,
|
|
51245
|
-
dimension: match.column.dimension
|
|
51246
|
-
measure: match.column.measure
|
|
51367
|
+
dimension: match.column.dimension
|
|
51247
51368
|
})));
|
|
51248
51369
|
} else {
|
|
51249
51370
|
const col = ConsumerManager.searchFieldInColumns(field.cField, availableColumns, consumer);
|
|
51250
51371
|
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
|
|
51372
|
+
expandedFields.push({ ...field, dimension: col.dimension });
|
|
51252
51373
|
}
|
|
51253
51374
|
return expandedFields;
|
|
51254
51375
|
};
|
|
@@ -52310,10 +52431,9 @@ var init_FieldLineage = __esm({
|
|
|
52310
52431
|
this._fromProducer = (producer, field) => {
|
|
52311
52432
|
const node = LineageEngine_default.nodeId("producer", producer.name);
|
|
52312
52433
|
const dimension = (producer.dimensions ?? []).find((x2) => x2.name === field.key);
|
|
52313
|
-
const
|
|
52314
|
-
const applied = dimension?.alias ? [`source column "${dimension.alias}"`] : measure ? ["measure"] : [];
|
|
52434
|
+
const applied = dimension?.alias ? [`source column "${dimension.alias}"`] : [];
|
|
52315
52435
|
const step = { resource: producer.name, node, field: field.key, applied };
|
|
52316
|
-
if (dimension
|
|
52436
|
+
if (dimension) return { path: [step] };
|
|
52317
52437
|
if (field.key === "*") return { path: [{ ...step, applied: ["every column of this producer"] }] };
|
|
52318
52438
|
if (Algo_default.isGlob(field.key)) return { path: [{ ...step, applied: ["every matching column"] }] };
|
|
52319
52439
|
return { path: [step], unresolved: `"${field.key}" is not a column of producer "${producer.name}"` };
|
|
@@ -52359,11 +52479,8 @@ var init_FieldLineage = __esm({
|
|
|
52359
52479
|
...field.hidden ? ["hidden from the export"] : []
|
|
52360
52480
|
];
|
|
52361
52481
|
};
|
|
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
|
-
];
|
|
52482
|
+
/** Every column a producer exposes, in declared order. */
|
|
52483
|
+
this._producerColumns = (producer) => (producer.dimensions ?? []).map((x2) => x2.name);
|
|
52367
52484
|
/** Whether a resource exposes a column by that name. A `*` consumer exposes whatever it is asked. */
|
|
52368
52485
|
this._exposes = (env2, name, column) => {
|
|
52369
52486
|
const producer = (env2.producers ?? []).find((x2) => x2.name === name);
|
|
@@ -52694,7 +52811,6 @@ var init_ResourceView = __esm({
|
|
|
52694
52811
|
],
|
|
52695
52812
|
tables: [
|
|
52696
52813
|
this._dimensionsTable(producer),
|
|
52697
|
-
...producer.measures?.length ? [this._measuresTable(producer)] : [],
|
|
52698
52814
|
this._usedByTable(env2, producer),
|
|
52699
52815
|
this._keyValueTable("Settings", settings)
|
|
52700
52816
|
],
|
|
@@ -52731,16 +52847,6 @@ var init_ResourceView = __esm({
|
|
|
52731
52847
|
]);
|
|
52732
52848
|
return this._table("Dimensions", columns, rows);
|
|
52733
52849
|
};
|
|
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
52850
|
/**
|
|
52745
52851
|
* Which consumers read each column, the downstream half of field-level lineage. It is the answer
|
|
52746
52852
|
* to "can this column go", which is a question a wide layout raises constantly and which nothing
|
|
@@ -54986,6 +55092,145 @@ var init_RunOverlay = __esm({
|
|
|
54986
55092
|
}
|
|
54987
55093
|
});
|
|
54988
55094
|
|
|
55095
|
+
// ../../packages/engines/src/parsing/ParquetExporter.ts
|
|
55096
|
+
var fs24, PARQUET_TYPES, TRUE_VALUES2, ParquetExporterClass, ParquetExporter, ParquetExporter_default;
|
|
55097
|
+
var init_ParquetExporter = __esm({
|
|
55098
|
+
"../../packages/engines/src/parsing/ParquetExporter.ts"() {
|
|
55099
|
+
fs24 = __toESM(require("fs"), 1);
|
|
55100
|
+
init_src();
|
|
55101
|
+
init_src3();
|
|
55102
|
+
init_src5();
|
|
55103
|
+
init_src2();
|
|
55104
|
+
PARQUET_TYPES = {
|
|
55105
|
+
string: "STRING",
|
|
55106
|
+
number: "DOUBLE",
|
|
55107
|
+
boolean: "BOOLEAN",
|
|
55108
|
+
datetime: "TIMESTAMP"
|
|
55109
|
+
};
|
|
55110
|
+
TRUE_VALUES2 = /^(1|true|yes|y|t)$/i;
|
|
55111
|
+
ParquetExporterClass = class {
|
|
55112
|
+
constructor() {
|
|
55113
|
+
/**
|
|
55114
|
+
* Writes `request.sourcePath` to `request.targetPath` as parquet, SNAPPY compressed, one typed
|
|
55115
|
+
* column per visible consumer field.
|
|
55116
|
+
*
|
|
55117
|
+
* Peak memory is bounded by one row group rather than by the dataset: the writer pulls the rows
|
|
55118
|
+
* a group at a time from the generator below, and nothing holds the file as a whole.
|
|
55119
|
+
*/
|
|
55120
|
+
this.exportToParquet = async (request) => {
|
|
55121
|
+
Affirm_default(request, "Missing required parameter request");
|
|
55122
|
+
Affirm_default(request.sourcePath, "Missing required parameter request.sourcePath");
|
|
55123
|
+
Affirm_default(request.targetPath, "Missing required parameter request.targetPath");
|
|
55124
|
+
Affirm_default(request.dsSchema, "Missing required parameter request.dsSchema");
|
|
55125
|
+
Affirm_default(request.consumerName, "Missing required parameter request.consumerName");
|
|
55126
|
+
const columns = this._columns(request.dsSchema);
|
|
55127
|
+
Affirm_default.hasItems(columns, `Consumer "${request.consumerName}" has no visible field to export as parquet`);
|
|
55128
|
+
const { parquetWriteRows, fileWriter } = await import("hyparquet-writer");
|
|
55129
|
+
const rowGroupSize = this._rowGroupSize(columns.length);
|
|
55130
|
+
const startedMs = Date.now();
|
|
55131
|
+
await parquetWriteRows({
|
|
55132
|
+
writer: fileWriter(request.targetPath),
|
|
55133
|
+
columns,
|
|
55134
|
+
rows: this._readRecords(request, columns),
|
|
55135
|
+
rowGroupSize
|
|
55136
|
+
});
|
|
55137
|
+
const bytes = fs24.statSync(request.targetPath).size;
|
|
55138
|
+
Logger_default.info(`Wrote parquet "${request.targetPath}" for consumer "${request.consumerName}" (${columns.length} column(s), ${rowGroupSize} rows/group, ${bytes} bytes, ${Date.now() - startedMs}ms)`);
|
|
55139
|
+
};
|
|
55140
|
+
/**
|
|
55141
|
+
* The exported schema, taken from the dataset's own: one column per column ON the line, in line
|
|
55142
|
+
* order. Not from the consumer's fields, which cannot know about a shape a `pivot` changed.
|
|
55143
|
+
*/
|
|
55144
|
+
this._columns = (dsSchema) => {
|
|
55145
|
+
return dsSchema.columns.map((column) => ({
|
|
55146
|
+
name: column.name,
|
|
55147
|
+
type: PARQUET_TYPES[column.type] ?? "STRING",
|
|
55148
|
+
nullable: true
|
|
55149
|
+
}));
|
|
55150
|
+
};
|
|
55151
|
+
/**
|
|
55152
|
+
* Rows per row group, derived from a budget of VALUES rather than a fixed row count: a row group
|
|
55153
|
+
* is buffered whole before it can be compressed, so what bounds the export's memory is cells, and
|
|
55154
|
+
* 100,000 rows means 400k values on a 4-column consumer and 130 million on a 1,300-field one.
|
|
55155
|
+
* Clamped at both ends — see the PARQUET_ constants for why each bound is there.
|
|
55156
|
+
*/
|
|
55157
|
+
this._rowGroupSize = (columnCount) => {
|
|
55158
|
+
Affirm_default(columnCount > 0, "Cannot size a row group for zero columns");
|
|
55159
|
+
const { PARQUET_ROW_GROUP_CELLS, PARQUET_MIN_ROW_GROUP_ROWS, PARQUET_MAX_ROW_GROUP_ROWS } = Constants_default.defaults;
|
|
55160
|
+
const rows = Math.floor(PARQUET_ROW_GROUP_CELLS / columnCount);
|
|
55161
|
+
return Math.min(Math.max(rows, PARQUET_MIN_ROW_GROUP_ROWS), PARQUET_MAX_ROW_GROUP_ROWS);
|
|
55162
|
+
};
|
|
55163
|
+
/** A positional line read onto the column names by position — the file holds the visible columns only. */
|
|
55164
|
+
this._positionalValues = (line, columns) => {
|
|
55165
|
+
const parts = DatasetCodec_default.decodeRow(line);
|
|
55166
|
+
const values = {};
|
|
55167
|
+
for (const [index, column] of columns.entries())
|
|
55168
|
+
values[column.name] = parts[index];
|
|
55169
|
+
return values;
|
|
55170
|
+
};
|
|
55171
|
+
/**
|
|
55172
|
+
* One value as its column holds it. A missing value is a null in the column rather than the type's
|
|
55173
|
+
* zero, so a reader can still tell "no value" from a real `0`, `false` or empty string.
|
|
55174
|
+
*
|
|
55175
|
+
* Missing is null/undefined, plus — for every type but `string` — the empty string, matching
|
|
55176
|
+
* `TypeCaster.castDimension`: that is how a NULL arrives from every source that stages through
|
|
55177
|
+
* text. An empty string IS a value for a string column and is kept.
|
|
55178
|
+
*/
|
|
55179
|
+
this._toParquetValue = (value, column, consumerName) => {
|
|
55180
|
+
if (value === null || value === void 0) return null;
|
|
55181
|
+
if (value === "" && column.type !== "STRING") return null;
|
|
55182
|
+
switch (column.type) {
|
|
55183
|
+
case "BOOLEAN":
|
|
55184
|
+
return typeof value === "boolean" ? value : TRUE_VALUES2.test(String(value).trim());
|
|
55185
|
+
case "DOUBLE":
|
|
55186
|
+
return typeof value === "number" ? value : Number(value);
|
|
55187
|
+
case "TIMESTAMP":
|
|
55188
|
+
return this._toDate(value, column.name, consumerName);
|
|
55189
|
+
default:
|
|
55190
|
+
return typeof value === "string" ? value : String(value);
|
|
55191
|
+
}
|
|
55192
|
+
};
|
|
55193
|
+
/**
|
|
55194
|
+
* A `datetime` column's value as a Date. An unparseable one FAILS the run, naming the field and
|
|
55195
|
+
* the value: a consumer whose transformation reformats a date into "DDMMYYYY" under a `datetime`
|
|
55196
|
+
* dimension is a configuration error, and a timestamp column silently full of 1970 hides it until
|
|
55197
|
+
* someone queries the lake.
|
|
55198
|
+
*/
|
|
55199
|
+
this._toDate = (value, name, consumerName) => {
|
|
55200
|
+
const date2 = value instanceof Date ? value : new Date(value);
|
|
55201
|
+
if (isNaN(date2.getTime()))
|
|
55202
|
+
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.`);
|
|
55203
|
+
return date2;
|
|
55204
|
+
};
|
|
55205
|
+
}
|
|
55206
|
+
/**
|
|
55207
|
+
* The dataset's records, one at a time, coerced to what each column holds. An async generator, so
|
|
55208
|
+
* the writer pulls a row group's worth and no more — the file is never in memory as a whole.
|
|
55209
|
+
*
|
|
55210
|
+
* A parquet consumer stages JSON (see `DatasetSchema.encodingFor`), so the values arrive already
|
|
55211
|
+
* typed. The positional branch covers a dataset staged the other way, reading the row WITHOUT
|
|
55212
|
+
* casting since the cast is what loses a boolean (`Boolean('false')` is `true`).
|
|
55213
|
+
*/
|
|
55214
|
+
async *_readRecords(request, columns) {
|
|
55215
|
+
const readStream = fs24.createReadStream(request.sourcePath);
|
|
55216
|
+
try {
|
|
55217
|
+
for await (const line of readLines(readStream)) {
|
|
55218
|
+
const values = request.dsSchema.encoding === "CSV" ? this._positionalValues(line, columns) : JSON.parse(line);
|
|
55219
|
+
const row = {};
|
|
55220
|
+
for (const column of columns)
|
|
55221
|
+
row[column.name] = this._toParquetValue(values[column.name], column, request.consumerName);
|
|
55222
|
+
yield row;
|
|
55223
|
+
}
|
|
55224
|
+
} finally {
|
|
55225
|
+
readStream.destroy();
|
|
55226
|
+
}
|
|
55227
|
+
}
|
|
55228
|
+
};
|
|
55229
|
+
ParquetExporter = new ParquetExporterClass();
|
|
55230
|
+
ParquetExporter_default = ParquetExporter;
|
|
55231
|
+
}
|
|
55232
|
+
});
|
|
55233
|
+
|
|
54989
55234
|
// ../../packages/engines/src/producer/PushdownEngine.ts
|
|
54990
55235
|
var MISSING_BY_TYPE, PushdownEngineClass, PushdownEngine, PushdownEngine_default;
|
|
54991
55236
|
var init_PushdownEngine = __esm({
|
|
@@ -55818,6 +56063,7 @@ var init_src10 = __esm({
|
|
|
55818
56063
|
init_RunOverlay();
|
|
55819
56064
|
init_RunRegistry();
|
|
55820
56065
|
init_LineParser();
|
|
56066
|
+
init_ParquetExporter();
|
|
55821
56067
|
init_DiscoverMerger();
|
|
55822
56068
|
init_ProducerEngine();
|
|
55823
56069
|
init_ProducerManager();
|
|
@@ -55833,6 +56079,108 @@ var init_src10 = __esm({
|
|
|
55833
56079
|
}
|
|
55834
56080
|
});
|
|
55835
56081
|
|
|
56082
|
+
// ../../packages/executors/src/DatasetSchema.ts
|
|
56083
|
+
var DatasetSchemaClass, DatasetSchema, DatasetSchema_default;
|
|
56084
|
+
var init_DatasetSchema = __esm({
|
|
56085
|
+
"../../packages/executors/src/DatasetSchema.ts"() {
|
|
56086
|
+
init_src();
|
|
56087
|
+
init_src10();
|
|
56088
|
+
DatasetSchemaClass = class {
|
|
56089
|
+
constructor() {
|
|
56090
|
+
/**
|
|
56091
|
+
* How a consumer's records are staged on the way to the destination. A consumer declares exactly
|
|
56092
|
+
* one output (`maxItems: 1` in consumer-schema.json), so `outputs[0]` is THE output.
|
|
56093
|
+
*/
|
|
56094
|
+
this.encodingFor = (consumer) => {
|
|
56095
|
+
Affirm_default(consumer, "Invalid consumer");
|
|
56096
|
+
Affirm_default.hasItems(consumer.outputs, `Consumer "${consumer.name}" declares no output`);
|
|
56097
|
+
switch (consumer.outputs[0].format) {
|
|
56098
|
+
case "JSON":
|
|
56099
|
+
case "API":
|
|
56100
|
+
return "JSON";
|
|
56101
|
+
case "PARQUET":
|
|
56102
|
+
return "JSON";
|
|
56103
|
+
case "TXT":
|
|
56104
|
+
return "CSV";
|
|
56105
|
+
case "CSV":
|
|
56106
|
+
return "CSV";
|
|
56107
|
+
default:
|
|
56108
|
+
throw new Error(`Export format ${consumer.outputs[0].format} not implemented yet.`);
|
|
56109
|
+
}
|
|
56110
|
+
};
|
|
56111
|
+
/**
|
|
56112
|
+
* The schema the run's workers write with: one column per VISIBLE consumer field, in field
|
|
56113
|
+
* order. Hidden fields are not on the line at all.
|
|
56114
|
+
*/
|
|
56115
|
+
this.compile = (consumer, fields) => {
|
|
56116
|
+
Affirm_default(consumer, "Invalid consumer");
|
|
56117
|
+
Affirm_default(fields, "Invalid consumer fields");
|
|
56118
|
+
const columns = fields.filter((x2) => !x2.cField.hidden).map(this._column);
|
|
56119
|
+
Affirm_default.hasItems(columns, `Consumer "${consumer.name}" has no visible field to write`);
|
|
56120
|
+
return {
|
|
56121
|
+
encoding: this.encodingFor(consumer),
|
|
56122
|
+
columns,
|
|
56123
|
+
rows: 0,
|
|
56124
|
+
writtenBy: "worker"
|
|
56125
|
+
};
|
|
56126
|
+
};
|
|
56127
|
+
/**
|
|
56128
|
+
* The same shape, as the named stage left it. Returned by every pass that rewrites the dataset
|
|
56129
|
+
* without changing its columns, so `rows` always describes the file as it is NOW.
|
|
56130
|
+
*/
|
|
56131
|
+
this.rewritten = (dsSchema, writtenBy, rows) => {
|
|
56132
|
+
Affirm_default(dsSchema, "Invalid dataset schema");
|
|
56133
|
+
Affirm_default(rows >= 0, `Invalid row count ${rows} for a dataset written by "${writtenBy}"`);
|
|
56134
|
+
return { ...dsSchema, writtenBy, rows };
|
|
56135
|
+
};
|
|
56136
|
+
/**
|
|
56137
|
+
* The shape a `pivot` leaves behind: the row keys it grouped on, then one column per pivot value.
|
|
56138
|
+
* A pivoted column has no producer dimension behind it - it is an aggregate this pass invented -
|
|
56139
|
+
* so it declares `source: 'pivot'` and a `number` type.
|
|
56140
|
+
*/
|
|
56141
|
+
this.forPivot = (dsSchema, rowKeys, pivotColumns, rows) => {
|
|
56142
|
+
Affirm_default(dsSchema, "Invalid dataset schema");
|
|
56143
|
+
Affirm_default.hasItems(rowKeys, "A pivot declares no row key");
|
|
56144
|
+
const keyColumns = rowKeys.map((key) => {
|
|
56145
|
+
const existing = dsSchema.columns.find((column) => column.name === key);
|
|
56146
|
+
Affirm_default(existing, `The pivot row key "${key}" is not a column of the dataset being pivoted`);
|
|
56147
|
+
return existing;
|
|
56148
|
+
});
|
|
56149
|
+
return {
|
|
56150
|
+
encoding: dsSchema.encoding,
|
|
56151
|
+
columns: [...keyColumns, ...pivotColumns.map((name) => ({ name, type: "number", source: "pivot" }))],
|
|
56152
|
+
rows,
|
|
56153
|
+
writtenBy: "pivot"
|
|
56154
|
+
};
|
|
56155
|
+
};
|
|
56156
|
+
/** The column names on a line, in line order - the header of a positional export. */
|
|
56157
|
+
this.names = (dsSchema) => {
|
|
56158
|
+
Affirm_default(dsSchema, "Invalid dataset schema");
|
|
56159
|
+
return dsSchema.columns.map((column) => column.name);
|
|
56160
|
+
};
|
|
56161
|
+
/**
|
|
56162
|
+
* One column, typed from the dimension behind it. `type`, `format` and `nullable` are the only
|
|
56163
|
+
* three properties a cast reads, so carrying those lets a reader cast a staged value back
|
|
56164
|
+
* without going near the consumer config.
|
|
56165
|
+
*/
|
|
56166
|
+
this._column = (field) => {
|
|
56167
|
+
if (ConsumerManager_default.isLineCountField(field.cField))
|
|
56168
|
+
return { name: field.finalKey, type: "number", source: "lineCount" };
|
|
56169
|
+
const column = {
|
|
56170
|
+
name: field.finalKey,
|
|
56171
|
+
type: field.dimension?.type ?? "string"
|
|
56172
|
+
};
|
|
56173
|
+
if (field.dimension?.format) column.format = field.dimension.format;
|
|
56174
|
+
if (field.dimension?.nullable === false) column.nullable = false;
|
|
56175
|
+
return column;
|
|
56176
|
+
};
|
|
56177
|
+
}
|
|
56178
|
+
};
|
|
56179
|
+
DatasetSchema = new DatasetSchemaClass();
|
|
56180
|
+
DatasetSchema_default = DatasetSchema;
|
|
56181
|
+
}
|
|
56182
|
+
});
|
|
56183
|
+
|
|
55836
56184
|
// ../../packages/executors/src/OutputExecutor.ts
|
|
55837
56185
|
var OutputExecutorClass, OutputExecutor, OutputExecutor_default;
|
|
55838
56186
|
var init_OutputExecutor = __esm({
|
|
@@ -55841,35 +56189,24 @@ var init_OutputExecutor = __esm({
|
|
|
55841
56189
|
init_src();
|
|
55842
56190
|
init_src8();
|
|
55843
56191
|
init_src4();
|
|
56192
|
+
init_src10();
|
|
55844
56193
|
init_src5();
|
|
55845
56194
|
init_src2();
|
|
56195
|
+
init_DatasetSchema();
|
|
55846
56196
|
OutputExecutorClass = class {
|
|
55847
56197
|
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
56198
|
/**
|
|
55860
|
-
* The
|
|
55861
|
-
*
|
|
55862
|
-
* all three for every record, and `toCSV` allocated a filtered field array on top of that.
|
|
56199
|
+
* The write side of a staged dataset's schema: what to write, in what order, in which encoding.
|
|
56200
|
+
* Derived from the schema, so the writer and every reader share one column list.
|
|
55863
56201
|
*/
|
|
55864
|
-
this.compileOutput = (consumer,
|
|
56202
|
+
this.compileOutput = (consumer, dsSchema) => {
|
|
55865
56203
|
Affirm_default(consumer, "Invalid consumer");
|
|
55866
|
-
Affirm_default(
|
|
56204
|
+
Affirm_default(dsSchema, "Invalid dataset schema");
|
|
55867
56205
|
return {
|
|
55868
|
-
format:
|
|
56206
|
+
format: dsSchema.encoding,
|
|
55869
56207
|
delimiter: ",",
|
|
55870
|
-
visibleKeys:
|
|
55871
|
-
|
|
55872
|
-
omitNull: consumer.outputs.some((x2) => x2.omitNull === true)
|
|
56208
|
+
visibleKeys: DatasetSchema_default.names(dsSchema),
|
|
56209
|
+
omitNull: consumer.outputs[0].omitNull === true
|
|
55873
56210
|
};
|
|
55874
56211
|
};
|
|
55875
56212
|
this.outputRecord = (record, plan) => {
|
|
@@ -55882,26 +56219,38 @@ var init_OutputExecutor = __esm({
|
|
|
55882
56219
|
throw new Error(`Export format ${plan.format} not implemented yet.`);
|
|
55883
56220
|
}
|
|
55884
56221
|
};
|
|
56222
|
+
/**
|
|
56223
|
+
* One positional line of the internal format. `DatasetCodec`, not `CSVParser`: a staged line has
|
|
56224
|
+
* to carry a null that is not an empty string and must never hold a newline.
|
|
56225
|
+
*/
|
|
55885
56226
|
this.toCSV = (record, plan) => {
|
|
55886
56227
|
const { visibleKeys, delimiter } = plan;
|
|
55887
56228
|
let row = "";
|
|
55888
56229
|
for (let index = 0; index < visibleKeys.length; index++) {
|
|
55889
56230
|
if (index > 0)
|
|
55890
56231
|
row += delimiter;
|
|
55891
|
-
row +=
|
|
56232
|
+
row += DatasetCodec_default.encodeField(record[visibleKeys[index]]);
|
|
55892
56233
|
}
|
|
55893
56234
|
return row;
|
|
55894
56235
|
};
|
|
56236
|
+
/**
|
|
56237
|
+
* One JSON object per line, keyed in the CONSUMER's field order rather than the record's own.
|
|
56238
|
+
*
|
|
56239
|
+
* A record arrives keyed in the order its producer's dimensions were read, so two producers of
|
|
56240
|
+
* one consumer wrote the same record as two different lines — and `distinct` and
|
|
56241
|
+
* `no_duplicates`, which compare the line, saw two rows. Building from `visibleKeys` gives every
|
|
56242
|
+
* writer one order, and drops the hidden fields without deleting them out of the caller's record.
|
|
56243
|
+
*/
|
|
55895
56244
|
this.toJSON = (record, plan) => {
|
|
55896
|
-
|
|
55897
|
-
|
|
55898
|
-
|
|
55899
|
-
|
|
55900
|
-
|
|
55901
|
-
|
|
55902
|
-
|
|
56245
|
+
const { visibleKeys, omitNull } = plan;
|
|
56246
|
+
const output = {};
|
|
56247
|
+
for (const key of visibleKeys) {
|
|
56248
|
+
const value = record[key];
|
|
56249
|
+
if (omitNull && (value === null || value === void 0))
|
|
56250
|
+
continue;
|
|
56251
|
+
output[key] = value === void 0 ? null : value;
|
|
55903
56252
|
}
|
|
55904
|
-
return JSON.stringify(
|
|
56253
|
+
return JSON.stringify(output);
|
|
55905
56254
|
};
|
|
55906
56255
|
/**
|
|
55907
56256
|
* The exported name of one split file: the destination name with the file's label before the
|
|
@@ -55914,66 +56263,80 @@ var init_OutputExecutor = __esm({
|
|
|
55914
56263
|
return `${pathName.substring(0, dot)}_${label}${pathName.substring(dot)}`;
|
|
55915
56264
|
};
|
|
55916
56265
|
/**
|
|
55917
|
-
* Writes the run's result to
|
|
56266
|
+
* Writes the run's result to the consumer's destination, one call per file the split pass
|
|
55918
56267
|
* produced. The files are passed in rather than read back out of the scope folder: the label a
|
|
55919
56268
|
* file carries is what names it at the destination, and with no split configured that list is
|
|
55920
56269
|
* the merged dataset alone, labelled "0" — the suffix an unsplit export has always had.
|
|
56270
|
+
*
|
|
56271
|
+
* One output per consumer (`maxItems: 1` in consumer-schema.json). This used to loop, but only
|
|
56272
|
+
* ever shipped the first entry, and could not have done more: `move` unlinks what it ships.
|
|
55921
56273
|
*/
|
|
55922
|
-
this.exportResult = async (consumer,
|
|
56274
|
+
this.exportResult = async (consumer, dsSchema, splitFiles) => {
|
|
55923
56275
|
Affirm_default.hasItems(splitFiles, `Consumer "${consumer.name}" has no output file to export`);
|
|
55924
|
-
|
|
56276
|
+
Affirm_default.hasItems(consumer.outputs, `Consumer "${consumer.name}" declares no output to export to`);
|
|
56277
|
+
Affirm_default(dsSchema, `Consumer "${consumer.name}" has no dataset schema to export against`);
|
|
56278
|
+
const output = consumer.outputs[0];
|
|
56279
|
+
const internalFormat = dsSchema.encoding;
|
|
56280
|
+
const destination = Environment_default.getSource(output.exportDestination);
|
|
56281
|
+
Affirm_default(destination, `The export destination "${output.exportDestination}" of consumer "${consumer.name}" is not a source of this project`);
|
|
56282
|
+
const driver = await DriverFactory_default.instantiateDestination(destination);
|
|
56283
|
+
const destinationName = this._composeFileName(consumer, output, this._getExtension(output));
|
|
56284
|
+
Logger_default.info(`Exporting consumer "${consumer.name}" to "${output.exportDestination}" as ${output.format} (${destinationName}, ${splitFiles.length} file(s))`);
|
|
56285
|
+
const header = this._composeHeader(output, dsSchema);
|
|
55925
56286
|
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
|
-
break;
|
|
55946
|
-
case "TXT": {
|
|
55947
|
-
const delimiter = this._getDelimiter(output);
|
|
55948
|
-
results.push(
|
|
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
|
-
}
|
|
56287
|
+
for (const { path: startingPath, label } of splitFiles) {
|
|
56288
|
+
const destinationPath = this.getCompletedPath(destinationName, label);
|
|
56289
|
+
if (output.format === internalFormat || output.format === "API") {
|
|
56290
|
+
results.push(await driver.move(startingPath, destinationPath, header));
|
|
56291
|
+
continue;
|
|
56292
|
+
}
|
|
56293
|
+
switch (output.format) {
|
|
56294
|
+
case "TXT": {
|
|
56295
|
+
const delimiter = this._getDelimiter(output);
|
|
56296
|
+
results.push(
|
|
56297
|
+
await driver.transformAndMove(
|
|
56298
|
+
startingPath,
|
|
56299
|
+
(line) => DatasetCodec_default.redelimit(line, delimiter),
|
|
56300
|
+
destinationPath,
|
|
56301
|
+
header
|
|
56302
|
+
)
|
|
56303
|
+
);
|
|
56304
|
+
break;
|
|
55973
56305
|
}
|
|
56306
|
+
case "PARQUET":
|
|
56307
|
+
results.push(await this._exportParquet({
|
|
56308
|
+
sourcePath: startingPath,
|
|
56309
|
+
destinationPath,
|
|
56310
|
+
dsSchema,
|
|
56311
|
+
consumerName: consumer.name,
|
|
56312
|
+
driver
|
|
56313
|
+
}));
|
|
56314
|
+
break;
|
|
56315
|
+
default:
|
|
56316
|
+
throw new Error(`Export result to format ${output.format} not implemented yet.`);
|
|
55974
56317
|
}
|
|
55975
|
-
return this._unifingResult(results);
|
|
55976
56318
|
}
|
|
56319
|
+
return this._unifingResult(results);
|
|
56320
|
+
};
|
|
56321
|
+
/**
|
|
56322
|
+
* Ships one split file as parquet. A parquet file is a columnar container whose footer can only
|
|
56323
|
+
* be written once the last row group is encoded, so there is no per-line mapping to hand a
|
|
56324
|
+
* driver: the file is built in the scope folder beside the dataset it is read from, then handed
|
|
56325
|
+
* over as bytes with NO header — a parquet file carries its own schema, and `move` is
|
|
56326
|
+
* byte-faithful in all three destination drivers.
|
|
56327
|
+
*
|
|
56328
|
+
* `move` unlinks what it shipped, so a successful export leaves no staged file behind. A failed
|
|
56329
|
+
* one leaves it in the scope folder, which the run's cleanup removes with everything else.
|
|
56330
|
+
*/
|
|
56331
|
+
this._exportParquet = async (plan) => {
|
|
56332
|
+
const parquetPath = `${plan.sourcePath}.parquet`;
|
|
56333
|
+
await ParquetExporter_default.exportToParquet({
|
|
56334
|
+
sourcePath: plan.sourcePath,
|
|
56335
|
+
targetPath: parquetPath,
|
|
56336
|
+
dsSchema: plan.dsSchema,
|
|
56337
|
+
consumerName: plan.consumerName
|
|
56338
|
+
});
|
|
56339
|
+
return plan.driver.move(parquetPath, plan.destinationPath);
|
|
55977
56340
|
};
|
|
55978
56341
|
/**
|
|
55979
56342
|
* Folds the per-file upload results into the one the run reports.
|
|
@@ -55995,21 +56358,28 @@ var init_OutputExecutor = __esm({
|
|
|
55995
56358
|
* does. Null for a JSON/API output, where a record already carries its own field names, and for
|
|
55996
56359
|
* an output that turns the header off.
|
|
55997
56360
|
*
|
|
55998
|
-
* The names
|
|
55999
|
-
* of the file has nothing else to match a column against.
|
|
56361
|
+
* The names come from the dataset's own schema, so a `pivot` is described by the header.
|
|
56000
56362
|
*/
|
|
56001
|
-
this._composeHeader = (output,
|
|
56363
|
+
this._composeHeader = (output, dsSchema) => {
|
|
56002
56364
|
if (output.header === false) return null;
|
|
56003
56365
|
if (output.format !== "CSV" && output.format !== "TXT") return null;
|
|
56004
|
-
|
|
56005
|
-
return CSVParser_default.stringifyRow(names, this._getDelimiter(output));
|
|
56366
|
+
return CSVParser_default.stringifyRow(DatasetSchema_default.names(dsSchema), this._getDelimiter(output));
|
|
56006
56367
|
};
|
|
56007
56368
|
/** The delimiter the exported file is written with. Only TXT configures it; CSV is a comma. */
|
|
56008
56369
|
this._getDelimiter = (output) => {
|
|
56009
56370
|
return output.format === "TXT" ? output.delimiter ?? " " : ",";
|
|
56010
56371
|
};
|
|
56011
56372
|
this._getExtension = (output) => {
|
|
56012
|
-
|
|
56373
|
+
switch (output.format) {
|
|
56374
|
+
case "CSV":
|
|
56375
|
+
return "csv";
|
|
56376
|
+
case "JSON":
|
|
56377
|
+
return "jsonl";
|
|
56378
|
+
case "PARQUET":
|
|
56379
|
+
return "parquet";
|
|
56380
|
+
default:
|
|
56381
|
+
return "txt";
|
|
56382
|
+
}
|
|
56013
56383
|
};
|
|
56014
56384
|
this._composeFileName = (consumer, output, extension, executionId) => {
|
|
56015
56385
|
if (output.exportName && output.exportName.trim().length > 0) {
|
|
@@ -56041,7 +56411,7 @@ var init_ConsumerExecutor = __esm({
|
|
|
56041
56411
|
init_src();
|
|
56042
56412
|
init_src10();
|
|
56043
56413
|
init_OutputExecutor();
|
|
56044
|
-
|
|
56414
|
+
init_DatasetSchema();
|
|
56045
56415
|
init_src2();
|
|
56046
56416
|
LIMIT_SCAN_BYTES = 64 * 1024;
|
|
56047
56417
|
NEWLINE_BYTE = 10;
|
|
@@ -56165,13 +56535,20 @@ var init_ConsumerExecutor = __esm({
|
|
|
56165
56535
|
}
|
|
56166
56536
|
return output;
|
|
56167
56537
|
};
|
|
56168
|
-
|
|
56169
|
-
|
|
56170
|
-
|
|
56171
|
-
|
|
56172
|
-
|
|
56173
|
-
|
|
56174
|
-
|
|
56538
|
+
/**
|
|
56539
|
+
* Rewrites every `lineCount` field of the merged dataset so it names the line of the SOURCE file
|
|
56540
|
+
* the row came from, by adding each chunk's `rowOffset` to the counter its worker wrote.
|
|
56541
|
+
*
|
|
56542
|
+
* The shape is unchanged, so the schema comes back with a fresh `writtenBy` and the row count
|
|
56543
|
+
* this pass actually saw - which also checks the file still holds what the workers reported.
|
|
56544
|
+
*/
|
|
56545
|
+
this.processLineCount = async (consumer, dsSchema, datasetPath, executorResults) => {
|
|
56546
|
+
Affirm_default(dsSchema, "Invalid dataset schema");
|
|
56547
|
+
const lineCountColumns = dsSchema.columns.filter((column) => column.source === "lineCount");
|
|
56548
|
+
if (lineCountColumns.length === 0)
|
|
56549
|
+
return dsSchema;
|
|
56550
|
+
const parseLine = LineParser_default.datasetParser(dsSchema);
|
|
56551
|
+
const outputPlan = OutputExecutor_default.compileOutput(consumer, dsSchema);
|
|
56175
56552
|
const tempWorkPath = datasetPath + "_tmp";
|
|
56176
56553
|
const reader = import_fs21.default.createReadStream(datasetPath);
|
|
56177
56554
|
const writer = import_fs21.default.createWriteStream(tempWorkPath);
|
|
@@ -56193,11 +56570,11 @@ var init_ConsumerExecutor = __esm({
|
|
|
56193
56570
|
Affirm_default(currentWorker, `Unable to resolve worker metadata while processing lineCount for ${datasetPath}`);
|
|
56194
56571
|
const rowOffset = currentWorker.rowOffset ?? 0;
|
|
56195
56572
|
if (rowOffset > 0) {
|
|
56196
|
-
const record =
|
|
56197
|
-
for (const
|
|
56198
|
-
const currentValue = Number(record[
|
|
56573
|
+
const record = parseLine(line);
|
|
56574
|
+
for (const column of lineCountColumns) {
|
|
56575
|
+
const currentValue = Number(record[column.name]);
|
|
56199
56576
|
Affirm_default(!Number.isNaN(currentValue), `Invalid lineCount value found while processing ${datasetPath}`);
|
|
56200
|
-
record[
|
|
56577
|
+
record[column.name] = currentValue + rowOffset;
|
|
56201
56578
|
}
|
|
56202
56579
|
if (!writer.write(OutputExecutor_default.outputRecord(record, outputPlan) + "\n"))
|
|
56203
56580
|
await waitForDrain();
|
|
@@ -56220,11 +56597,11 @@ var init_ConsumerExecutor = __esm({
|
|
|
56220
56597
|
}
|
|
56221
56598
|
await import_promises16.default.unlink(datasetPath);
|
|
56222
56599
|
await import_promises16.default.rename(tempWorkPath, datasetPath);
|
|
56223
|
-
return processedLineCount;
|
|
56600
|
+
return DatasetSchema_default.rewritten(dsSchema, "lineCount", processedLineCount);
|
|
56224
56601
|
};
|
|
56225
56602
|
/**
|
|
56226
|
-
* Cuts the merged dataset down to the run's record limit, and reports the
|
|
56227
|
-
*
|
|
56603
|
+
* Cuts the merged dataset down to the run's record limit, and reports the shape it left behind.
|
|
56604
|
+
* The schema comes back unchanged when the dataset was already within the limit.
|
|
56228
56605
|
*
|
|
56229
56606
|
* Every worker stops at the limit within its OWN chunk, which is the right thing for the read
|
|
56230
56607
|
* (no thread reads further than the run could need) but leaves up to `limit` rows per chunk in
|
|
@@ -56234,7 +56611,8 @@ var init_ConsumerExecutor = __esm({
|
|
|
56234
56611
|
* at then needs no arithmetic on line contents, so no line ending or multi-byte character can
|
|
56235
56612
|
* put the cut in the middle of a row. Only the bytes up to the limit are ever read.
|
|
56236
56613
|
*/
|
|
56237
|
-
this.processLimit = async (datasetPath, limit) => {
|
|
56614
|
+
this.processLimit = async (dsSchema, datasetPath, limit) => {
|
|
56615
|
+
Affirm_default(dsSchema, "Invalid dataset schema");
|
|
56238
56616
|
Affirm_default(datasetPath, "Invalid dataset path");
|
|
56239
56617
|
Affirm_default(limit > 0, `Invalid record limit ${limit}`);
|
|
56240
56618
|
const handle = await import_promises16.default.open(datasetPath, "r+");
|
|
@@ -56245,7 +56623,7 @@ var init_ConsumerExecutor = __esm({
|
|
|
56245
56623
|
while (newlines < limit) {
|
|
56246
56624
|
const { bytesRead } = await handle.read(buffer, 0, LIMIT_SCAN_BYTES, offset);
|
|
56247
56625
|
if (bytesRead === 0)
|
|
56248
|
-
return
|
|
56626
|
+
return dsSchema;
|
|
56249
56627
|
for (let index = 0; index < bytesRead; index++) {
|
|
56250
56628
|
offset++;
|
|
56251
56629
|
if (buffer[index] !== NEWLINE_BYTE) continue;
|
|
@@ -56255,14 +56633,15 @@ var init_ConsumerExecutor = __esm({
|
|
|
56255
56633
|
}
|
|
56256
56634
|
const { size } = await handle.stat();
|
|
56257
56635
|
if (offset >= size)
|
|
56258
|
-
return
|
|
56636
|
+
return dsSchema;
|
|
56259
56637
|
await handle.truncate(offset);
|
|
56260
|
-
return limit;
|
|
56638
|
+
return DatasetSchema_default.rewritten(dsSchema, "limit", limit);
|
|
56261
56639
|
} finally {
|
|
56262
56640
|
await handle.close();
|
|
56263
56641
|
}
|
|
56264
56642
|
};
|
|
56265
|
-
this.processDistinct = async (datasetPath) => {
|
|
56643
|
+
this.processDistinct = async (dsSchema, datasetPath) => {
|
|
56644
|
+
Affirm_default(dsSchema, "Invalid dataset schema");
|
|
56266
56645
|
const reader = import_fs21.default.createReadStream(datasetPath);
|
|
56267
56646
|
const tempWorkPath = datasetPath + "_tmp";
|
|
56268
56647
|
const writer = import_fs21.default.createWriteStream(tempWorkPath);
|
|
@@ -56291,18 +56670,18 @@ var init_ConsumerExecutor = __esm({
|
|
|
56291
56670
|
}
|
|
56292
56671
|
await import_promises16.default.unlink(datasetPath);
|
|
56293
56672
|
await import_promises16.default.rename(tempWorkPath, datasetPath);
|
|
56294
|
-
return newLineCount;
|
|
56673
|
+
return DatasetSchema_default.rewritten(dsSchema, "distinct", newLineCount);
|
|
56295
56674
|
};
|
|
56296
|
-
this.processDistinctOn = async (consumer, datasetPath) => {
|
|
56675
|
+
this.processDistinctOn = async (consumer, dsSchema, datasetPath) => {
|
|
56676
|
+
Affirm_default(dsSchema, "Invalid dataset schema");
|
|
56297
56677
|
const reader = import_fs21.default.createReadStream(datasetPath);
|
|
56298
56678
|
const { distinctOn } = consumer.options;
|
|
56299
56679
|
const { keys, resolution } = distinctOn;
|
|
56300
56680
|
const { strategy, orderBy, direction = "asc" } = resolution;
|
|
56301
|
-
const
|
|
56302
|
-
const internalFields = ConsumerManager_default.getExpandedFields(consumer);
|
|
56681
|
+
const parseLine = LineParser_default.datasetParser(dsSchema);
|
|
56303
56682
|
const winners = /* @__PURE__ */ new Map();
|
|
56304
56683
|
for await (const line of readLines(reader)) {
|
|
56305
|
-
const record =
|
|
56684
|
+
const record = parseLine(line);
|
|
56306
56685
|
const compositeKey = keys.map((k7) => String(record[k7] ?? "")).join("|");
|
|
56307
56686
|
const existing = winners.get(compositeKey);
|
|
56308
56687
|
if (!existing) {
|
|
@@ -56334,20 +56713,27 @@ var init_ConsumerExecutor = __esm({
|
|
|
56334
56713
|
}
|
|
56335
56714
|
await import_promises16.default.unlink(datasetPath);
|
|
56336
56715
|
await import_promises16.default.rename(tempWorkPath, datasetPath);
|
|
56337
|
-
return winners.size;
|
|
56716
|
+
return DatasetSchema_default.rewritten(dsSchema, "distinctOn", winners.size);
|
|
56338
56717
|
};
|
|
56339
|
-
|
|
56718
|
+
/**
|
|
56719
|
+
* Groups the dataset by `rowKeys` and turns one column's values into columns of their own.
|
|
56720
|
+
*
|
|
56721
|
+
* This is the one pass that changes the SHAPE of the file, which is why it returns the schema it
|
|
56722
|
+
* wrote rather than a row count: the header, the split pass, the dataset validation and the
|
|
56723
|
+
* parquet schema all read the result.
|
|
56724
|
+
*/
|
|
56725
|
+
this.processPivot = async (consumer, dsSchema, datasetPath) => {
|
|
56726
|
+
Affirm_default(dsSchema, "Invalid dataset schema");
|
|
56340
56727
|
const { pivot } = consumer.options;
|
|
56341
56728
|
const { rowKeys, pivotColumn, valueColumn, aggregation, columnPrefix = "" } = pivot;
|
|
56342
|
-
const
|
|
56343
|
-
const internalFields = ConsumerManager_default.getExpandedFields(consumer);
|
|
56729
|
+
const parseLine = LineParser_default.datasetParser(dsSchema);
|
|
56344
56730
|
let pivotValues = pivot.pivotValues;
|
|
56345
56731
|
if (!pivotValues) {
|
|
56346
56732
|
pivotValues = [];
|
|
56347
56733
|
const discoverySet = /* @__PURE__ */ new Set();
|
|
56348
56734
|
const discoverReader = import_fs21.default.createReadStream(datasetPath);
|
|
56349
56735
|
for await (const line of readLines(discoverReader)) {
|
|
56350
|
-
const record =
|
|
56736
|
+
const record = parseLine(line);
|
|
56351
56737
|
const val = String(record[pivotColumn] ?? "");
|
|
56352
56738
|
if (!discoverySet.has(val)) {
|
|
56353
56739
|
discoverySet.add(val);
|
|
@@ -56364,7 +56750,7 @@ var init_ConsumerExecutor = __esm({
|
|
|
56364
56750
|
const groups = /* @__PURE__ */ new Map();
|
|
56365
56751
|
const reader = import_fs21.default.createReadStream(datasetPath);
|
|
56366
56752
|
for await (const line of readLines(reader)) {
|
|
56367
|
-
const record =
|
|
56753
|
+
const record = parseLine(line);
|
|
56368
56754
|
const compositeKey = rowKeys.map((k7) => String(record[k7] ?? "")).join("|");
|
|
56369
56755
|
const pivotVal = String(record[pivotColumn] ?? "");
|
|
56370
56756
|
const numericVal = Number(record[valueColumn]) || 0;
|
|
@@ -56383,11 +56769,8 @@ var init_ConsumerExecutor = __esm({
|
|
|
56383
56769
|
cell.min = Math.min(cell.min, numericVal);
|
|
56384
56770
|
cell.max = Math.max(cell.max, numericVal);
|
|
56385
56771
|
}
|
|
56386
|
-
const
|
|
56387
|
-
|
|
56388
|
-
...pivotValues.map((pv) => ({ cField: { key: columnPrefix + pv }, finalKey: columnPrefix + pv }))
|
|
56389
|
-
];
|
|
56390
|
-
const outputPlan = OutputExecutor_default.compileOutput(consumer, pivotedFields);
|
|
56772
|
+
const pivotedDsSchema = DatasetSchema_default.forPivot(dsSchema, rowKeys, pivotValues.map((pv) => columnPrefix + pv), groups.size);
|
|
56773
|
+
const outputPlan = OutputExecutor_default.compileOutput(consumer, pivotedDsSchema);
|
|
56391
56774
|
const tempWorkPath = datasetPath + "_tmp";
|
|
56392
56775
|
const writer = import_fs21.default.createWriteStream(tempWorkPath);
|
|
56393
56776
|
const waitForDrain = () => new Promise((resolve) => writer.once("drain", resolve));
|
|
@@ -56437,10 +56820,8 @@ var init_ConsumerExecutor = __esm({
|
|
|
56437
56820
|
}
|
|
56438
56821
|
await import_promises16.default.unlink(datasetPath);
|
|
56439
56822
|
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);
|
|
56823
|
+
Affirm_default(outputCount === pivotedDsSchema.rows, `Pivot wrote ${outputCount} row(s) but declared ${pivotedDsSchema.rows}`);
|
|
56824
|
+
return pivotedDsSchema;
|
|
56444
56825
|
};
|
|
56445
56826
|
/**
|
|
56446
56827
|
* Determines if the new record should replace the existing record based on the resolution strategy
|
|
@@ -56467,11 +56848,11 @@ var init_ConsumerExecutor = __esm({
|
|
|
56467
56848
|
return false;
|
|
56468
56849
|
}
|
|
56469
56850
|
};
|
|
56470
|
-
this.processDatasetValidation = async (consumer, datasetPath) => {
|
|
56851
|
+
this.processDatasetValidation = async (consumer, dsSchema, datasetPath) => {
|
|
56852
|
+
Affirm_default(dsSchema, "Invalid dataset schema");
|
|
56471
56853
|
const validations = consumer.validate;
|
|
56472
56854
|
if (!validations || validations.length === 0) return [];
|
|
56473
|
-
const
|
|
56474
|
-
const internalFields = ConsumerManager_default.getExpandedFields(consumer);
|
|
56855
|
+
const parseLine = LineParser_default.datasetParser(dsSchema);
|
|
56475
56856
|
let rowCount = 0;
|
|
56476
56857
|
const seenRows = /* @__PURE__ */ new Set();
|
|
56477
56858
|
const fieldValueSets = /* @__PURE__ */ new Map();
|
|
@@ -56492,7 +56873,7 @@ var init_ConsumerExecutor = __esm({
|
|
|
56492
56873
|
seenRows.add(line);
|
|
56493
56874
|
}
|
|
56494
56875
|
if (uniqueFieldKeys.length > 0) {
|
|
56495
|
-
const record =
|
|
56876
|
+
const record = parseLine(line);
|
|
56496
56877
|
for (const fieldKey of uniqueFieldKeys) {
|
|
56497
56878
|
const valueSet = fieldValueSets.get(fieldKey);
|
|
56498
56879
|
const val = String(record[fieldKey] ?? "");
|
|
@@ -56629,14 +57010,13 @@ var init_ExecutorPerformance = __esm({
|
|
|
56629
57010
|
});
|
|
56630
57011
|
|
|
56631
57012
|
// ../../packages/executors/src/ExecutorWriter.ts
|
|
56632
|
-
var
|
|
57013
|
+
var fs27, ExecutorWriter, ExecutorWriter_default;
|
|
56633
57014
|
var init_ExecutorWriter = __esm({
|
|
56634
57015
|
"../../packages/executors/src/ExecutorWriter.ts"() {
|
|
56635
|
-
|
|
57016
|
+
fs27 = __toESM(require("fs"));
|
|
56636
57017
|
init_src();
|
|
56637
57018
|
init_src3();
|
|
56638
57019
|
init_src10();
|
|
56639
|
-
init_OutputExecutor();
|
|
56640
57020
|
ExecutorWriter = class {
|
|
56641
57021
|
constructor() {
|
|
56642
57022
|
/**
|
|
@@ -56666,7 +57046,8 @@ var init_ExecutorWriter = __esm({
|
|
|
56666
57046
|
Affirm_default(request, "Invalid split request");
|
|
56667
57047
|
Affirm_default(request.sourcePath, "Missing dataset to split");
|
|
56668
57048
|
Affirm_default(request.consumer, "Invalid consumer");
|
|
56669
|
-
const { scope, sourcePath, consumer,
|
|
57049
|
+
const { scope, sourcePath, consumer, dsSchema } = request;
|
|
57050
|
+
Affirm_default(dsSchema, "Missing dataset schema to split against");
|
|
56670
57051
|
const keys = scope.split?.by ?? [];
|
|
56671
57052
|
const maxFileBytes = scope.split?.maxFileSizeGb > 0 ? scope.split.maxFileSizeGb * this.BYTES_PER_GB : null;
|
|
56672
57053
|
if (keys.length === 0 && !maxFileBytes)
|
|
@@ -56681,21 +57062,21 @@ var init_ExecutorWriter = __esm({
|
|
|
56681
57062
|
usedLabels: /* @__PURE__ */ new Set(),
|
|
56682
57063
|
fileCount: 0
|
|
56683
57064
|
};
|
|
56684
|
-
await this._route(pass,
|
|
57065
|
+
await this._route(pass, dsSchema);
|
|
56685
57066
|
if (pass.buckets.size === 0)
|
|
56686
57067
|
return [{ path: sourcePath, label: this.UNSPLIT_LABEL }];
|
|
56687
|
-
await
|
|
57068
|
+
await fs27.promises.unlink(sourcePath);
|
|
56688
57069
|
return this._listFiles(pass);
|
|
56689
57070
|
};
|
|
56690
57071
|
/**
|
|
56691
57072
|
* The pass itself: one line read, one line written, for every line of the dataset.
|
|
56692
57073
|
*/
|
|
56693
|
-
this._route = async (pass,
|
|
56694
|
-
const
|
|
56695
|
-
const readStream =
|
|
57074
|
+
this._route = async (pass, dsSchema) => {
|
|
57075
|
+
const parseLine = pass.keys.length === 0 ? null : LineParser_default.datasetParser(dsSchema);
|
|
57076
|
+
const readStream = fs27.createReadStream(pass.sourcePath);
|
|
56696
57077
|
try {
|
|
56697
57078
|
for await (const line of readLines(readStream)) {
|
|
56698
|
-
const bucket = this._bucketFor(line, pass,
|
|
57079
|
+
const bucket = this._bucketFor(line, pass, parseLine);
|
|
56699
57080
|
const lineBytes = Buffer.byteLength(line) + 1;
|
|
56700
57081
|
if (this._needsRotation(bucket, pass, lineBytes))
|
|
56701
57082
|
await this._rotate(bucket, pass);
|
|
@@ -56715,8 +57096,8 @@ var init_ExecutorWriter = __esm({
|
|
|
56715
57096
|
* own: a shared catch-all would silently merge two different things, and that is invisible until
|
|
56716
57097
|
* it is wrong.
|
|
56717
57098
|
*/
|
|
56718
|
-
this._bucketFor = (line, pass,
|
|
56719
|
-
const bucketKey =
|
|
57099
|
+
this._bucketFor = (line, pass, parseLine) => {
|
|
57100
|
+
const bucketKey = parseLine ? this._bucketKey(line, pass.keys, parseLine) : "";
|
|
56720
57101
|
const existing = pass.buckets.get(bucketKey);
|
|
56721
57102
|
if (existing) return existing;
|
|
56722
57103
|
const bucket = {
|
|
@@ -56730,8 +57111,8 @@ var init_ExecutorWriter = __esm({
|
|
|
56730
57111
|
return bucket;
|
|
56731
57112
|
};
|
|
56732
57113
|
/** The composite value a line buckets on, read off the parsed record. */
|
|
56733
|
-
this._bucketKey = (line, keys,
|
|
56734
|
-
const record =
|
|
57114
|
+
this._bucketKey = (line, keys, parseLine) => {
|
|
57115
|
+
const record = parseLine(line);
|
|
56735
57116
|
return keys.map((key) => {
|
|
56736
57117
|
const value = record[key];
|
|
56737
57118
|
if (value === null || value === void 0) return "_null";
|
|
@@ -56796,7 +57177,7 @@ var init_ExecutorWriter = __esm({
|
|
|
56796
57177
|
}
|
|
56797
57178
|
if (pass.openStreams.length >= Constants_default.defaults.MAX_OPEN_SPLIT_STREAMS)
|
|
56798
57179
|
await this._closeStream(pass.openStreams.shift());
|
|
56799
|
-
bucket.stream =
|
|
57180
|
+
bucket.stream = fs27.createWriteStream(this.getCompletedPath(pass.sourcePath, this._composeLabel(bucket, pass)), { flags: "a" });
|
|
56800
57181
|
pass.openStreams.push(bucket);
|
|
56801
57182
|
return bucket.stream;
|
|
56802
57183
|
};
|
|
@@ -57085,7 +57466,6 @@ var init_LookupResolver = __esm({
|
|
|
57085
57466
|
init_src4();
|
|
57086
57467
|
init_src10();
|
|
57087
57468
|
init_src2();
|
|
57088
|
-
init_OutputExecutor();
|
|
57089
57469
|
LookupResolverClass = class {
|
|
57090
57470
|
constructor() {
|
|
57091
57471
|
// Reuse dicts across sibling consumers within one process; invalidated when the config changes.
|
|
@@ -57108,28 +57488,35 @@ var init_LookupResolver = __esm({
|
|
|
57108
57488
|
Affirm_default(!stack.includes(ref.consumer), `code_lookup cycle detected: ${[...stack, ref.consumer].join(" -> ")}`);
|
|
57109
57489
|
const lookupConsumer = Environment_default.getConsumer(ref.consumer);
|
|
57110
57490
|
Affirm_default(lookupConsumer, `code_lookup references unknown consumer "${ref.consumer}"`);
|
|
57111
|
-
let
|
|
57112
|
-
if (!
|
|
57113
|
-
capturePath = this.getCapturePath(scope, ref.consumer);
|
|
57491
|
+
let capture = captureByConsumer.get(ref.consumer);
|
|
57492
|
+
if (!capture) {
|
|
57493
|
+
const capturePath = this.getCapturePath(scope, ref.consumer);
|
|
57114
57494
|
ExecutorScope_default.ensurePath(capturePath);
|
|
57115
|
-
|
|
57116
|
-
|
|
57495
|
+
capture = {
|
|
57496
|
+
path: capturePath,
|
|
57497
|
+
dsSchema: await this.runLookupConsumer(lookupConsumer, capturePath, stack, request)
|
|
57498
|
+
};
|
|
57499
|
+
captureByConsumer.set(ref.consumer, capture);
|
|
57117
57500
|
}
|
|
57118
|
-
this.cache.set(id, await this.buildDict(
|
|
57501
|
+
this.cache.set(id, await this.buildDict(capture, ref));
|
|
57119
57502
|
}
|
|
57120
57503
|
ExecutorScope_default.ensurePath(outPath);
|
|
57121
57504
|
await import_promises17.default.writeFile(outPath, JSON.stringify(this.cache.get(id)));
|
|
57122
57505
|
manifest.push({ id, consumer: ref.consumer, key: ref.key, value: ref.value, path: import_path22.default.resolve(outPath) });
|
|
57123
57506
|
}
|
|
57124
|
-
for (const
|
|
57125
|
-
await import_promises17.default.rm(
|
|
57507
|
+
for (const capture of captureByConsumer.values())
|
|
57508
|
+
await import_promises17.default.rm(capture.path, { force: true });
|
|
57126
57509
|
return manifest;
|
|
57127
57510
|
};
|
|
57128
|
-
|
|
57511
|
+
/**
|
|
57512
|
+
* Runs the source consumer through the real pipeline, capturing its merged output before
|
|
57513
|
+
* cleanup and returning the shape that run produced. The capture is a verbatim copy of the
|
|
57514
|
+
* dataset, so its schema is the ONLY description of it.
|
|
57515
|
+
*/
|
|
57129
57516
|
this.runLookupConsumer = async (lookupConsumer, capturePath, stack, request) => {
|
|
57130
57517
|
const { default: ExecutorOrchestrator2 } = await Promise.resolve().then(() => (init_ExecutorOrchestrator(), ExecutorOrchestrator_exports));
|
|
57131
57518
|
Logger_default.log(`[lookup] Materializing consumer "${lookupConsumer.name}" (path: ${stack.join(" -> ")})`);
|
|
57132
|
-
await ExecutorOrchestrator2.launch({
|
|
57519
|
+
const result = await ExecutorOrchestrator2.launch({
|
|
57133
57520
|
consumer: lookupConsumer,
|
|
57134
57521
|
details: request.details,
|
|
57135
57522
|
captureMainDatasetTo: capturePath,
|
|
@@ -57137,22 +57524,23 @@ var init_LookupResolver = __esm({
|
|
|
57137
57524
|
_lookupStack: stack,
|
|
57138
57525
|
syntheticProducerInputs: request.syntheticProducerInputs
|
|
57139
57526
|
});
|
|
57527
|
+
Affirm_default(result.dsSchema, `code_lookup source consumer "${lookupConsumer.name}" reported no dataset schema`);
|
|
57528
|
+
return result.dsSchema;
|
|
57140
57529
|
};
|
|
57141
57530
|
// 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}"`);
|
|
57531
|
+
this.buildDict = async (capture, ref) => {
|
|
57532
|
+
const columnNames = capture.dsSchema.columns.map((column) => column.name);
|
|
57533
|
+
Affirm_default(columnNames.includes(ref.key), `code_lookup key column "${ref.key}" is not in the output of consumer "${ref.consumer}"`);
|
|
57534
|
+
Affirm_default(columnNames.includes(ref.value), `code_lookup value column "${ref.value}" is not in the output of consumer "${ref.consumer}"`);
|
|
57535
|
+
const parseLine = LineParser_default.datasetParser(capture.dsSchema);
|
|
57148
57536
|
const dict = {};
|
|
57149
57537
|
let rowsRead = 0;
|
|
57150
57538
|
let warnedDup = false;
|
|
57151
|
-
for await (const line of readLines(import_fs23.default.createReadStream(
|
|
57539
|
+
for await (const line of readLines(import_fs23.default.createReadStream(capture.path))) {
|
|
57152
57540
|
if (!line.length) continue;
|
|
57153
57541
|
rowsRead++;
|
|
57154
57542
|
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 =
|
|
57543
|
+
const record = parseLine(line);
|
|
57156
57544
|
const code = String(record[ref.key]);
|
|
57157
57545
|
if (code in dict && !warnedDup) {
|
|
57158
57546
|
Logger_default.warn(`code_lookup: duplicate key(s) in consumer "${ref.consumer}" for "${ref.key}"; last value wins`);
|
|
@@ -57163,10 +57551,6 @@ var init_LookupResolver = __esm({
|
|
|
57163
57551
|
Logger_default.log(`[lookup] Built table "${ref.consumer}" (${ref.key}->${ref.value}): ${Object.keys(dict).length} entr(ies) from ${rowsRead} row(s)`);
|
|
57164
57552
|
return dict;
|
|
57165
57553
|
};
|
|
57166
|
-
this.parseLine = (line, format4, visibleFields) => {
|
|
57167
|
-
if (format4 === "CSV") return LineParser_default._internalParseCSV(line, visibleFields, ",");
|
|
57168
|
-
return JSON.parse(line);
|
|
57169
|
-
};
|
|
57170
57554
|
this.getCapturePath = (scope, consumerName) => {
|
|
57171
57555
|
const dir = import_path22.default.dirname(ExecutorScope_default.getLookupPath(scope, "x"));
|
|
57172
57556
|
return import_path22.default.join(dir, `${consumerName}.capture`);
|
|
@@ -57200,7 +57584,6 @@ var init_ConsumerInputResolver = __esm({
|
|
|
57200
57584
|
init_src();
|
|
57201
57585
|
init_src3();
|
|
57202
57586
|
init_src4();
|
|
57203
|
-
init_src10();
|
|
57204
57587
|
init_src2();
|
|
57205
57588
|
ConsumerInputResolverClass = class {
|
|
57206
57589
|
constructor() {
|
|
@@ -57225,23 +57608,29 @@ var init_ConsumerInputResolver = __esm({
|
|
|
57225
57608
|
Affirm_default(upstream, `consumer-as-input references unknown consumer "${name}"`);
|
|
57226
57609
|
const capturePath = ExecutorScope_default.getConsumerInputPath(scope, name);
|
|
57227
57610
|
ExecutorScope_default.ensurePath(capturePath);
|
|
57228
|
-
await this.runUpstreamConsumer(upstream, capturePath, stack, request);
|
|
57611
|
+
const dsSchema = await this.runUpstreamConsumer(upstream, capturePath, stack, request);
|
|
57229
57612
|
const { size } = await import_promises18.default.stat(capturePath);
|
|
57230
|
-
this.cache.set(name, { consumer: name, path: import_path23.default.resolve(capturePath), fileSize: size });
|
|
57613
|
+
this.cache.set(name, { consumer: name, path: import_path23.default.resolve(capturePath), fileSize: size, dsSchema });
|
|
57231
57614
|
}
|
|
57232
57615
|
manifest.set(name, this.cache.get(name));
|
|
57233
57616
|
}
|
|
57234
57617
|
return manifest;
|
|
57235
57618
|
};
|
|
57236
|
-
|
|
57237
|
-
|
|
57238
|
-
|
|
57239
|
-
|
|
57240
|
-
|
|
57619
|
+
/**
|
|
57620
|
+
* Builds the in-memory producer the orchestrator reads the JSONL capture through. Its dimensions
|
|
57621
|
+
* are the columns the upstream run REPORTED writing, not the consumer's declared fields, which a
|
|
57622
|
+
* `pivot` leaves describing a shape no longer in the file.
|
|
57623
|
+
*
|
|
57624
|
+
* No `format` is carried over: the capture holds values the upstream run already rendered, so
|
|
57625
|
+
* re-parsing them with that dimension's input format would fail on its own output.
|
|
57626
|
+
*/
|
|
57627
|
+
this.buildSyntheticProducer = (entry) => {
|
|
57628
|
+
Affirm_default(entry, "Invalid consumer input entry");
|
|
57629
|
+
Affirm_default(entry.dsSchema, `Consumer input "${entry.consumer}" carries no dataset schema`);
|
|
57241
57630
|
return {
|
|
57242
|
-
name:
|
|
57243
|
-
source: `__consumer_input__:${
|
|
57244
|
-
dimensions,
|
|
57631
|
+
name: entry.consumer,
|
|
57632
|
+
source: `__consumer_input__:${entry.consumer}`,
|
|
57633
|
+
dimensions: entry.dsSchema.columns.map((column) => ({ name: column.name, type: column.type })),
|
|
57245
57634
|
settings: { fileType: "JSONL" },
|
|
57246
57635
|
_version: 1
|
|
57247
57636
|
};
|
|
@@ -57256,11 +57645,12 @@ var init_ConsumerInputResolver = __esm({
|
|
|
57256
57645
|
}
|
|
57257
57646
|
return names;
|
|
57258
57647
|
};
|
|
57259
|
-
// Runs the upstream consumer through the real pipeline, capturing its
|
|
57648
|
+
// Runs the upstream consumer through the real pipeline, capturing its output as JSONL and
|
|
57649
|
+
// reporting the shape that run produced.
|
|
57260
57650
|
this.runUpstreamConsumer = async (upstream, capturePath, stack, request) => {
|
|
57261
57651
|
const { default: ExecutorOrchestrator2 } = await Promise.resolve().then(() => (init_ExecutorOrchestrator(), ExecutorOrchestrator_exports));
|
|
57262
57652
|
Logger_default.log(`[consumer-input] Materializing consumer "${upstream.name}" (path: ${stack.join(" -> ")})`);
|
|
57263
|
-
await ExecutorOrchestrator2.launch({
|
|
57653
|
+
const result = await ExecutorOrchestrator2.launch({
|
|
57264
57654
|
consumer: upstream,
|
|
57265
57655
|
details: request.details,
|
|
57266
57656
|
captureMainDatasetTo: capturePath,
|
|
@@ -57269,6 +57659,8 @@ var init_ConsumerInputResolver = __esm({
|
|
|
57269
57659
|
_consumerInputStack: stack,
|
|
57270
57660
|
syntheticProducerInputs: request.syntheticProducerInputs
|
|
57271
57661
|
});
|
|
57662
|
+
Affirm_default(result.dsSchema, `consumer-as-input source "${upstream.name}" reported no dataset schema`);
|
|
57663
|
+
return result.dsSchema;
|
|
57272
57664
|
};
|
|
57273
57665
|
this.invalidateCacheIfStale = () => {
|
|
57274
57666
|
const generation = Environment_default._configHash ?? null;
|
|
@@ -57310,6 +57702,7 @@ var init_ExecutorOrchestrator = __esm({
|
|
|
57310
57702
|
init_src();
|
|
57311
57703
|
init_src10();
|
|
57312
57704
|
init_ExecutorWriter();
|
|
57705
|
+
init_DatasetSchema();
|
|
57313
57706
|
import_promises20 = require("stream/promises");
|
|
57314
57707
|
init_src2();
|
|
57315
57708
|
init_ExecutorProgress2();
|
|
@@ -57328,9 +57721,9 @@ var init_ExecutorOrchestrator = __esm({
|
|
|
57328
57721
|
* spawn lazily, on first exec, and a thread that dies mid-run is replaced by another, so this is
|
|
57329
57722
|
* a count of spawns and NOT a concurrency figure (see IWorkerCensus).
|
|
57330
57723
|
*
|
|
57331
|
-
* `workerData` carries the loaded environment
|
|
57332
|
-
*
|
|
57333
|
-
* data goes in the exec argument,
|
|
57724
|
+
* `workerData` carries the loaded environment and the run's dataset schema, so both are cloned
|
|
57725
|
+
* once per THREAD at spawn rather than per chunk. Only run-constant data belongs there; per-task
|
|
57726
|
+
* data goes in the exec argument, cloned per chunk.
|
|
57334
57727
|
*/
|
|
57335
57728
|
this.createPool = (maxWorkers, census, threadData2) => {
|
|
57336
57729
|
const options2 = {
|
|
@@ -57361,8 +57754,9 @@ var init_ExecutorOrchestrator = __esm({
|
|
|
57361
57754
|
const tracker = new ExecutorPerformance_default();
|
|
57362
57755
|
const _progress = new ExecutorProgress2_default(logProgress);
|
|
57363
57756
|
const { usageId } = UsageManager_default.startUsage(consumer, details);
|
|
57364
|
-
const scope = { id: usageId, folder: `${consumer.name}_${usageId}`, workersId: [], split: OrchestratorHelper_default.resolveSplit(consumer) };
|
|
57757
|
+
const scope = ExecutorScope_default.open({ id: usageId, folder: `${consumer.name}_${usageId}`, workersId: [], split: OrchestratorHelper_default.resolveSplit(consumer) });
|
|
57365
57758
|
const expandedFields = ConsumerManager_default.getExpandedFields(consumer);
|
|
57759
|
+
let dsSchema = DatasetSchema_default.compile(consumer, expandedFields);
|
|
57366
57760
|
const census = { created: 0, peak: 0 };
|
|
57367
57761
|
try {
|
|
57368
57762
|
const start = performance.now();
|
|
@@ -57397,6 +57791,7 @@ var init_ExecutorOrchestrator = __esm({
|
|
|
57397
57791
|
executorResults.push(...await this.runWorkFiles({
|
|
57398
57792
|
workFiles,
|
|
57399
57793
|
consumer,
|
|
57794
|
+
dsSchema,
|
|
57400
57795
|
scope,
|
|
57401
57796
|
options: options2,
|
|
57402
57797
|
lookups,
|
|
@@ -57411,58 +57806,26 @@ var init_ExecutorOrchestrator = __esm({
|
|
|
57411
57806
|
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
57807
|
_progress.update({ phase: "Reconciling work", progress: 0 });
|
|
57413
57808
|
await this.reconcileExecutorThreadsResults(scope, executorResults, tracker);
|
|
57809
|
+
dsSchema = DatasetSchema_default.rewritten(dsSchema, "merge", Algo_default.sum(executorResults.map((x2) => x2.outputCount)));
|
|
57414
57810
|
_progress.update({ phase: "Reconciling work", progress: 1 });
|
|
57415
57811
|
Logger_default.log(`[${usageId}] Reconciled ${executorResults.length} worker result(s)`);
|
|
57416
57812
|
_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
|
-
}
|
|
57813
|
+
dsSchema = await this.runPostPasses({ consumer, dsSchema, scope, executorResults, tracker, usageId });
|
|
57451
57814
|
if (options2?.limit > 0) {
|
|
57452
57815
|
counter = performance.now();
|
|
57453
|
-
const
|
|
57816
|
+
const limited = await ConsumerExecutor_default.processLimit(dsSchema, ExecutorScope_default.getMainPath(scope), options2.limit);
|
|
57454
57817
|
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`);
|
|
57818
|
+
if (limited.writtenBy === "limit") {
|
|
57819
|
+
Logger_default.log(`[${usageId}] Record limit applied: dataset cut to ${limited.rows} row(s) in ${Math.round(performance.now() - counter)}ms`);
|
|
57458
57820
|
} else {
|
|
57459
57821
|
Logger_default.log(`[${usageId}] Record limit of ${options2.limit} not reached \u2014 dataset left whole`);
|
|
57460
57822
|
}
|
|
57823
|
+
dsSchema = limited;
|
|
57461
57824
|
}
|
|
57462
57825
|
if (consumer.validate && consumer.validate.length > 0) {
|
|
57463
57826
|
Logger_default.log(`[${usageId}] Running dataset-level validations`);
|
|
57464
57827
|
counter = performance.now();
|
|
57465
|
-
const validationResults = await ConsumerExecutor_default.processDatasetValidation(consumer, ExecutorScope_default.getMainPath(scope));
|
|
57828
|
+
const validationResults = await ConsumerExecutor_default.processDatasetValidation(consumer, dsSchema, ExecutorScope_default.getMainPath(scope));
|
|
57466
57829
|
tracker.measure("dataset-validation", performance.now() - counter);
|
|
57467
57830
|
for (const result of validationResults) {
|
|
57468
57831
|
if (result.onFail === "fail") {
|
|
@@ -57476,7 +57839,7 @@ var init_ExecutorOrchestrator = __esm({
|
|
|
57476
57839
|
Logger_default.log(`[${usageId}] Dataset validations complete in ${Math.round(performance.now() - counter)}ms`);
|
|
57477
57840
|
}
|
|
57478
57841
|
if (request.captureMainDatasetTo) {
|
|
57479
|
-
await this.captureDataset(
|
|
57842
|
+
await this.captureDataset(dsSchema, ExecutorScope_default.getMainPath(scope), request.captureMainDatasetTo, request.captureFormat);
|
|
57480
57843
|
Logger_default.log(`[${usageId}] Captured dataset to ${request.captureMainDatasetTo}${request.captureFormat ? ` (as ${request.captureFormat})` : ""}`);
|
|
57481
57844
|
}
|
|
57482
57845
|
if (request.skipExport) {
|
|
@@ -57485,8 +57848,8 @@ var init_ExecutorOrchestrator = __esm({
|
|
|
57485
57848
|
const materializeResult = OrchestratorHelper_default.computeFinalResult(tracker, executorResults, usageId, null);
|
|
57486
57849
|
materializeResult.elapsedMS = performance.now() - start;
|
|
57487
57850
|
materializeResult.peakWorkers = census.peak;
|
|
57488
|
-
|
|
57489
|
-
|
|
57851
|
+
materializeResult.outputCount = dsSchema.rows;
|
|
57852
|
+
materializeResult.dsSchema = dsSchema;
|
|
57490
57853
|
UsageManager_default.endUsage(usageId, materializeResult.outputCount, materializeResult);
|
|
57491
57854
|
await Logger_default.flush();
|
|
57492
57855
|
return materializeResult;
|
|
@@ -57496,7 +57859,7 @@ var init_ExecutorOrchestrator = __esm({
|
|
|
57496
57859
|
scope,
|
|
57497
57860
|
sourcePath: ExecutorScope_default.getMainPath(scope),
|
|
57498
57861
|
consumer,
|
|
57499
|
-
|
|
57862
|
+
dsSchema
|
|
57500
57863
|
});
|
|
57501
57864
|
tracker.measure("split-output", performance.now() - counter);
|
|
57502
57865
|
if (scope.split)
|
|
@@ -57505,7 +57868,7 @@ var init_ExecutorOrchestrator = __esm({
|
|
|
57505
57868
|
_progress.update({ phase: "Exporting result", progress: 0 });
|
|
57506
57869
|
counter = performance.now();
|
|
57507
57870
|
Logger_default.info(`[${usageId}] Exporting results to ${consumer.outputs.length} output(s)`);
|
|
57508
|
-
const exportRes = await OutputExecutor_default.exportResult(consumer,
|
|
57871
|
+
const exportRes = await OutputExecutor_default.exportResult(consumer, dsSchema, splitFiles);
|
|
57509
57872
|
tracker.measure("export-result", performance.now() - counter);
|
|
57510
57873
|
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
57874
|
Logger_default.info(`[${usageId}] Export complete in ${Math.round(performance.now() - counter)}ms (key: ${exportRes.key})`);
|
|
@@ -57522,8 +57885,8 @@ var init_ExecutorOrchestrator = __esm({
|
|
|
57522
57885
|
const finalResult = OrchestratorHelper_default.computeFinalResult(tracker, executorResults, usageId, exportRes.key);
|
|
57523
57886
|
finalResult.elapsedMS = performance.now() - start;
|
|
57524
57887
|
finalResult.peakWorkers = census.peak;
|
|
57525
|
-
|
|
57526
|
-
|
|
57888
|
+
finalResult.outputCount = dsSchema.rows;
|
|
57889
|
+
finalResult.dsSchema = dsSchema;
|
|
57527
57890
|
UsageManager_default.endUsage(usageId, finalResult.outputCount, finalResult);
|
|
57528
57891
|
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
57892
|
await Logger_default.flush();
|
|
@@ -57601,7 +57964,10 @@ var init_ExecutorOrchestrator = __esm({
|
|
|
57601
57964
|
`Invalid worker pool size ${poolSize} \u2014 it must come from getParallelWorkerCount(), never from the work list`
|
|
57602
57965
|
);
|
|
57603
57966
|
let phase = performance.now();
|
|
57604
|
-
const pool = this.createPool(poolSize, census, {
|
|
57967
|
+
const pool = this.createPool(poolSize, census, {
|
|
57968
|
+
environment: Environment_default.createSnapshot(),
|
|
57969
|
+
dsSchema: request.dsSchema
|
|
57970
|
+
});
|
|
57605
57971
|
tracker.measure("pool-create", performance.now() - phase);
|
|
57606
57972
|
const context = {
|
|
57607
57973
|
consumer: request.consumer,
|
|
@@ -57824,24 +58190,23 @@ var init_ExecutorOrchestrator = __esm({
|
|
|
57824
58190
|
};
|
|
57825
58191
|
/**
|
|
57826
58192
|
* 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
|
-
*
|
|
58193
|
+
* captureFormat 'JSONL' the output is normalized to newline-delimited JSON objects keyed by the
|
|
58194
|
+
* dataset's own column names — self-describing, so a synthetic producer can read it without a
|
|
57829
58195
|
* header row. main.dataset in JSON internal format is already JSONL (verbatim copy); CSV internal
|
|
57830
58196
|
* format has no header row, so each row is reprojected. Without a captureFormat it's a verbatim copy.
|
|
57831
58197
|
*/
|
|
57832
|
-
this.captureDataset = async (
|
|
58198
|
+
this.captureDataset = async (dsSchema, sourcePath, targetPath, captureFormat) => {
|
|
57833
58199
|
await import_promises19.default.mkdir(import_path24.default.dirname(targetPath), { recursive: true });
|
|
57834
|
-
|
|
57835
|
-
if (captureFormat !== "JSONL" || internalFormat !== "CSV") {
|
|
58200
|
+
if (captureFormat !== "JSONL" || dsSchema.encoding !== "CSV") {
|
|
57836
58201
|
await import_promises19.default.copyFile(sourcePath, targetPath);
|
|
57837
58202
|
return;
|
|
57838
58203
|
}
|
|
57839
|
-
const
|
|
58204
|
+
const parseLine = LineParser_default.datasetParser(dsSchema);
|
|
57840
58205
|
const output = import_fs25.default.createWriteStream(targetPath);
|
|
57841
58206
|
try {
|
|
57842
58207
|
for await (const line of readLines(import_fs25.default.createReadStream(sourcePath))) {
|
|
57843
58208
|
if (!line.length) continue;
|
|
57844
|
-
const record =
|
|
58209
|
+
const record = parseLine(line);
|
|
57845
58210
|
if (!output.write(JSON.stringify(record) + "\n"))
|
|
57846
58211
|
await new Promise((resolve) => output.once("drain", resolve));
|
|
57847
58212
|
}
|
|
@@ -57854,7 +58219,7 @@ var init_ExecutorOrchestrator = __esm({
|
|
|
57854
58219
|
}
|
|
57855
58220
|
};
|
|
57856
58221
|
/**
|
|
57857
|
-
* Grabs the remote datasets and
|
|
58222
|
+
* Grabs the remote datasets and stages each one as a local `.extract` to work on
|
|
57858
58223
|
*/
|
|
57859
58224
|
this.readySourceFiles = async (args) => {
|
|
57860
58225
|
const { consumer, scope, consumerInputs, syntheticInputs, options: options2, onProgress } = args;
|
|
@@ -57864,7 +58229,7 @@ var init_ExecutorOrchestrator = __esm({
|
|
|
57864
58229
|
const reportProducer = (fraction) => onProgress?.((i6 + fraction) / consumer.producers.length);
|
|
57865
58230
|
const consumerInput = consumerInputs.get(cProd.name);
|
|
57866
58231
|
if (consumerInput) {
|
|
57867
|
-
const prod2 = ConsumerInputResolver_default.buildSyntheticProducer(
|
|
58232
|
+
const prod2 = ConsumerInputResolver_default.buildSyntheticProducer(consumerInput);
|
|
57868
58233
|
const response2 = { files: [{ fullUri: consumerInput.path, fileSize: consumerInput.fileSize }] };
|
|
57869
58234
|
Logger_default.log(`[${scope.id}] Producer "${cProd.name}" is a consumer input: ${consumerInput.fileSize} bytes (${i6 + 1}/${consumer.producers.length})`);
|
|
57870
58235
|
results.push({ prod: prod2, cProd, response: response2 });
|
|
@@ -58013,6 +58378,49 @@ var init_ExecutorOrchestrator = __esm({
|
|
|
58013
58378
|
}
|
|
58014
58379
|
return mainPath;
|
|
58015
58380
|
};
|
|
58381
|
+
/**
|
|
58382
|
+
* The passes that work across lines, run on the merged dataset. Returns the schema the last
|
|
58383
|
+
* pass to write the file left behind — its `rows` is the run's row count.
|
|
58384
|
+
*
|
|
58385
|
+
* `lineCount` goes FIRST: it walks each chunk's `outputCount` in order to find the worker a line
|
|
58386
|
+
* came from, which only holds while the file still has sum(outputCount) lines in worker order.
|
|
58387
|
+
*/
|
|
58388
|
+
this.runPostPasses = async (request) => {
|
|
58389
|
+
const { consumer, scope, executorResults, tracker, usageId } = request;
|
|
58390
|
+
const datasetPath = ExecutorScope_default.getMainPath(scope);
|
|
58391
|
+
const isMerged = executorResults.length > 1;
|
|
58392
|
+
let dsSchema = request.dsSchema;
|
|
58393
|
+
let counter = 0;
|
|
58394
|
+
if (isMerged && ConsumerManager_default.hasLineCountFields(consumer, true)) {
|
|
58395
|
+
Logger_default.log(`[${usageId}] Running unified lineCount pass across merged workers`);
|
|
58396
|
+
counter = performance.now();
|
|
58397
|
+
dsSchema = await ConsumerExecutor_default.processLineCount(consumer, dsSchema, datasetPath, executorResults);
|
|
58398
|
+
tracker.measure("process-line-count:main", performance.now() - counter);
|
|
58399
|
+
Logger_default.log(`[${usageId}] LineCount pass complete: normalized ${dsSchema.rows} rows in ${Math.round(performance.now() - counter)}ms`);
|
|
58400
|
+
}
|
|
58401
|
+
if (isMerged && consumer.options?.distinct === true) {
|
|
58402
|
+
Logger_default.log(`[${usageId}] Running unified distinct pass across merged workers`);
|
|
58403
|
+
counter = performance.now();
|
|
58404
|
+
dsSchema = await ConsumerExecutor_default.processDistinct(dsSchema, datasetPath);
|
|
58405
|
+
tracker.measure("process-distinct:main", performance.now() - counter);
|
|
58406
|
+
Logger_default.log(`[${usageId}] Distinct pass complete: ${dsSchema.rows} unique rows in ${Math.round(performance.now() - counter)}ms`);
|
|
58407
|
+
}
|
|
58408
|
+
if (isMerged && consumer.options?.distinctOn) {
|
|
58409
|
+
Logger_default.log(`[${usageId}] Running unified distinctOn pass (${consumer.options.distinctOn})`);
|
|
58410
|
+
counter = performance.now();
|
|
58411
|
+
dsSchema = await ConsumerExecutor_default.processDistinctOn(consumer, dsSchema, datasetPath);
|
|
58412
|
+
tracker.measure("process-distinct-on:main", performance.now() - counter);
|
|
58413
|
+
Logger_default.log(`[${usageId}] DistinctOn pass complete: ${dsSchema.rows} rows in ${Math.round(performance.now() - counter)}ms`);
|
|
58414
|
+
}
|
|
58415
|
+
if (consumer.options?.pivot) {
|
|
58416
|
+
Logger_default.log(`[${usageId}] Running pivot operation`);
|
|
58417
|
+
counter = performance.now();
|
|
58418
|
+
dsSchema = await ConsumerExecutor_default.processPivot(consumer, dsSchema, datasetPath);
|
|
58419
|
+
tracker.measure("process-pivot:main", performance.now() - counter);
|
|
58420
|
+
Logger_default.log(`[${usageId}] Pivot complete: ${dsSchema.rows} rows in ${Math.round(performance.now() - counter)}ms`);
|
|
58421
|
+
}
|
|
58422
|
+
return dsSchema;
|
|
58423
|
+
};
|
|
58016
58424
|
this.performCleanupOperations = async (scope, tracker) => {
|
|
58017
58425
|
const start = performance.now();
|
|
58018
58426
|
await ExecutorScope_default.clearScope(scope);
|
|
@@ -58034,6 +58442,7 @@ var init_ExecutorOrchestrator = __esm({
|
|
|
58034
58442
|
// ../../packages/executors/src/index.ts
|
|
58035
58443
|
init_src();
|
|
58036
58444
|
init_ConsumerExecutor();
|
|
58445
|
+
init_DatasetSchema();
|
|
58037
58446
|
|
|
58038
58447
|
// ../../packages/executors/src/Executor.ts
|
|
58039
58448
|
var import_fs22 = __toESM(require("fs"));
|
|
@@ -58189,6 +58598,7 @@ var Executor = class {
|
|
|
58189
58598
|
this.run = async (request) => {
|
|
58190
58599
|
Affirm_default(request, "Invalid request");
|
|
58191
58600
|
const { consumer, producer, prodDimensions, workerId, chunk, options: options2, scope, reportWork, lookups } = request;
|
|
58601
|
+
Affirm_default(request.dsSchema, `Worker ${workerId} was given no dataset schema to write with`);
|
|
58192
58602
|
const recordLimit = this.canBoundChunk(consumer) ? options2?.limit : null;
|
|
58193
58603
|
this.hydrateLookups(lookups);
|
|
58194
58604
|
const counter = performance.now();
|
|
@@ -58212,7 +58622,8 @@ var Executor = class {
|
|
|
58212
58622
|
const writer = this.openWriteStream(scope, workerId);
|
|
58213
58623
|
const fields = ConsumerManager_default.getExpandedFields(consumer);
|
|
58214
58624
|
const plan = RecordPlan_default.compile({ consumer, producer, fields, dimensions: prodDimensions });
|
|
58215
|
-
|
|
58625
|
+
let dsSchema = request.dsSchema;
|
|
58626
|
+
const outputPlan = OutputExecutor_default.compileOutput(consumer, dsSchema);
|
|
58216
58627
|
const linePlan = ProducerExecutor_default.compileLine(prodDimensions, chunk);
|
|
58217
58628
|
const recordSelector = ProducerManager_default.resolveRecordSelector(producer);
|
|
58218
58629
|
const { isFirstChunk, start, end } = chunk;
|
|
@@ -58296,14 +58707,16 @@ var Executor = class {
|
|
|
58296
58707
|
if (consumer.options?.distinct === true) {
|
|
58297
58708
|
Logger_default.log(`[${workerId}] Running distinct pass`);
|
|
58298
58709
|
perf = performance.now();
|
|
58299
|
-
|
|
58710
|
+
dsSchema = await ConsumerExecutor_default.processDistinct(dsSchema, result.resultUri);
|
|
58711
|
+
totalOutputCount = dsSchema.rows;
|
|
58300
58712
|
this._performance.measure("process-distinct", performance.now() - perf);
|
|
58301
58713
|
totalCycles++;
|
|
58302
58714
|
}
|
|
58303
58715
|
if (consumer.options?.distinctOn) {
|
|
58304
58716
|
Logger_default.log(`[${workerId}] Running distinctOn pass (${consumer.options.distinctOn})`);
|
|
58305
58717
|
perf = performance.now();
|
|
58306
|
-
|
|
58718
|
+
dsSchema = await ConsumerExecutor_default.processDistinctOn(consumer, dsSchema, result.resultUri);
|
|
58719
|
+
totalOutputCount = dsSchema.rows;
|
|
58307
58720
|
this._performance.measure("process-distinct-on", performance.now() - perf);
|
|
58308
58721
|
totalCycles++;
|
|
58309
58722
|
}
|
|
@@ -58353,6 +58766,7 @@ var Executor_default = Executor;
|
|
|
58353
58766
|
// ../../packages/executors/src/index.ts
|
|
58354
58767
|
init_ExecutorOrchestrator();
|
|
58355
58768
|
init_ExecutorPerformance();
|
|
58769
|
+
init_ExecutorWriter();
|
|
58356
58770
|
init_OrchestratorHelper();
|
|
58357
58771
|
|
|
58358
58772
|
// ../../packages/executors/src/workers/ExecutorWorkerRunner.ts
|
|
@@ -58361,6 +58775,8 @@ var import_dotenv = __toESM(require("dotenv"));
|
|
|
58361
58775
|
var import_worker_threads = require("worker_threads");
|
|
58362
58776
|
init_src();
|
|
58363
58777
|
init_src4();
|
|
58778
|
+
init_src10();
|
|
58779
|
+
init_DatasetSchema();
|
|
58364
58780
|
init_src2();
|
|
58365
58781
|
import_dotenv.default.configDotenv();
|
|
58366
58782
|
var MODULE_LOADED_AT = performance.timeOrigin + performance.now();
|
|
@@ -58411,6 +58827,7 @@ var runExecutorTask = async (workerData) => {
|
|
|
58411
58827
|
chunk,
|
|
58412
58828
|
options: options2,
|
|
58413
58829
|
scope,
|
|
58830
|
+
dsSchema: resolveDsSchema(consumer),
|
|
58414
58831
|
lookups,
|
|
58415
58832
|
reportWork: (packet) => import_workerpool2.default.workerEmit(packet)
|
|
58416
58833
|
});
|
|
@@ -58431,6 +58848,11 @@ var runExecutorTask = async (workerData) => {
|
|
|
58431
58848
|
throw error;
|
|
58432
58849
|
}
|
|
58433
58850
|
};
|
|
58851
|
+
var resolveDsSchema = (consumer) => {
|
|
58852
|
+
const sent = import_worker_threads.workerData?.dsSchema;
|
|
58853
|
+
if (sent) return sent;
|
|
58854
|
+
return DatasetSchema_default.compile(consumer, ConsumerManager_default.getExpandedFields(consumer));
|
|
58855
|
+
};
|
|
58434
58856
|
var registerExecutorWorker = () => {
|
|
58435
58857
|
import_workerpool2.default.worker({
|
|
58436
58858
|
executor: runExecutorTask
|
|
@@ -58507,8 +58929,7 @@ var SampleRunnerClass = class {
|
|
|
58507
58929
|
captureMainDatasetTo: capturePath,
|
|
58508
58930
|
captureFormat: "JSONL"
|
|
58509
58931
|
});
|
|
58510
|
-
const
|
|
58511
|
-
const records = captured.map((record) => this._inFieldOrder(consumer, record));
|
|
58932
|
+
const records = await this._readCapture(capturePath, size);
|
|
58512
58933
|
Logger_default.log(`[sample] Consumer "${consumer.name}": ${records.length} row(s) from ${result.inputCount} input line(s)`);
|
|
58513
58934
|
return {
|
|
58514
58935
|
kind: "consumer",
|
|
@@ -58556,28 +58977,6 @@ var SampleRunnerClass = class {
|
|
|
58556
58977
|
return this._inputProducers(nested, seen);
|
|
58557
58978
|
});
|
|
58558
58979
|
};
|
|
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
58980
|
/** The captured dataset, which `captureFormat: 'JSONL'` guarantees is one JSON object per line. */
|
|
58582
58981
|
this._readCapture = async (capturePath, size) => {
|
|
58583
58982
|
if (!import_fs26.default.existsSync(capturePath)) return [];
|