@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
package/index.js
CHANGED
|
@@ -8490,7 +8490,7 @@ var require_node2 = __commonJS({
|
|
|
8490
8490
|
var require_tail_file = __commonJS({
|
|
8491
8491
|
"../../node_modules/winston/lib/winston/tail-file.js"(exports2, module2) {
|
|
8492
8492
|
"use strict";
|
|
8493
|
-
var
|
|
8493
|
+
var fs40 = require("fs");
|
|
8494
8494
|
var { StringDecoder } = require("string_decoder");
|
|
8495
8495
|
var { Stream } = require_readable();
|
|
8496
8496
|
function noop() {
|
|
@@ -8511,7 +8511,7 @@ var require_tail_file = __commonJS({
|
|
|
8511
8511
|
stream.emit("end");
|
|
8512
8512
|
stream.emit("close");
|
|
8513
8513
|
};
|
|
8514
|
-
|
|
8514
|
+
fs40.open(options2.file, "a+", "0644", (err2, fd) => {
|
|
8515
8515
|
if (err2) {
|
|
8516
8516
|
if (!iter) {
|
|
8517
8517
|
stream.emit("error", err2);
|
|
@@ -8523,10 +8523,10 @@ var require_tail_file = __commonJS({
|
|
|
8523
8523
|
}
|
|
8524
8524
|
(function read() {
|
|
8525
8525
|
if (stream.destroyed) {
|
|
8526
|
-
|
|
8526
|
+
fs40.close(fd, noop);
|
|
8527
8527
|
return;
|
|
8528
8528
|
}
|
|
8529
|
-
return
|
|
8529
|
+
return fs40.read(fd, buffer, 0, buffer.length, pos, (error, bytes) => {
|
|
8530
8530
|
if (error) {
|
|
8531
8531
|
if (!iter) {
|
|
8532
8532
|
stream.emit("error", error);
|
|
@@ -8585,7 +8585,7 @@ var require_tail_file = __commonJS({
|
|
|
8585
8585
|
var require_file = __commonJS({
|
|
8586
8586
|
"../../node_modules/winston/lib/winston/transports/file.js"(exports2, module2) {
|
|
8587
8587
|
"use strict";
|
|
8588
|
-
var
|
|
8588
|
+
var fs40 = require("fs");
|
|
8589
8589
|
var path30 = require("path");
|
|
8590
8590
|
var asyncSeries = require_series();
|
|
8591
8591
|
var zlib3 = require("zlib");
|
|
@@ -8790,7 +8790,7 @@ var require_file = __commonJS({
|
|
|
8790
8790
|
let buff = "";
|
|
8791
8791
|
let results = [];
|
|
8792
8792
|
let row = 0;
|
|
8793
|
-
const stream =
|
|
8793
|
+
const stream = fs40.createReadStream(file, {
|
|
8794
8794
|
encoding: "utf8"
|
|
8795
8795
|
});
|
|
8796
8796
|
stream.on("error", (err2) => {
|
|
@@ -8942,7 +8942,7 @@ var require_file = __commonJS({
|
|
|
8942
8942
|
stat(callback) {
|
|
8943
8943
|
const target = this._getFile();
|
|
8944
8944
|
const fullpath = path30.join(this.dirname, target);
|
|
8945
|
-
|
|
8945
|
+
fs40.stat(fullpath, (err2, stat) => {
|
|
8946
8946
|
if (err2 && err2.code === "ENOENT") {
|
|
8947
8947
|
debug("ENOENT\xA0ok", fullpath);
|
|
8948
8948
|
this.filename = target;
|
|
@@ -9047,7 +9047,7 @@ var require_file = __commonJS({
|
|
|
9047
9047
|
_createStream(source) {
|
|
9048
9048
|
const fullpath = path30.join(this.dirname, this.filename);
|
|
9049
9049
|
debug("create stream start", fullpath, this.options);
|
|
9050
|
-
const dest =
|
|
9050
|
+
const dest = fs40.createWriteStream(fullpath, this.options).on("error", (err2) => debug(err2)).on("close", () => debug("close", dest.path, dest.bytesWritten)).on("open", () => {
|
|
9051
9051
|
debug("file open ok", fullpath);
|
|
9052
9052
|
this.emit("open", fullpath);
|
|
9053
9053
|
source.pipe(dest);
|
|
@@ -9126,7 +9126,7 @@ var require_file = __commonJS({
|
|
|
9126
9126
|
const isZipped = this.zippedArchive ? ".gz" : "";
|
|
9127
9127
|
const filePath = `${basename}${isOldest}${ext}${isZipped}`;
|
|
9128
9128
|
const target = path30.join(this.dirname, filePath);
|
|
9129
|
-
|
|
9129
|
+
fs40.unlink(target, callback);
|
|
9130
9130
|
}
|
|
9131
9131
|
/**
|
|
9132
9132
|
* Roll files forward based on integer, up to maxFiles. e.g. if base if
|
|
@@ -9149,17 +9149,17 @@ var require_file = __commonJS({
|
|
|
9149
9149
|
tasks.push(function(i6, cb) {
|
|
9150
9150
|
let fileName = `${basename}${i6 - 1}${ext}${isZipped}`;
|
|
9151
9151
|
const tmppath = path30.join(this.dirname, fileName);
|
|
9152
|
-
|
|
9152
|
+
fs40.exists(tmppath, (exists) => {
|
|
9153
9153
|
if (!exists) {
|
|
9154
9154
|
return cb(null);
|
|
9155
9155
|
}
|
|
9156
9156
|
fileName = `${basename}${i6}${ext}${isZipped}`;
|
|
9157
|
-
|
|
9157
|
+
fs40.rename(tmppath, path30.join(this.dirname, fileName), cb);
|
|
9158
9158
|
});
|
|
9159
9159
|
}.bind(this, x2));
|
|
9160
9160
|
}
|
|
9161
9161
|
asyncSeries(tasks, () => {
|
|
9162
|
-
|
|
9162
|
+
fs40.rename(
|
|
9163
9163
|
path30.join(this.dirname, `${basename}${ext}${isZipped}`),
|
|
9164
9164
|
path30.join(this.dirname, `${basename}1${ext}${isZipped}`),
|
|
9165
9165
|
callback
|
|
@@ -9175,22 +9175,22 @@ var require_file = __commonJS({
|
|
|
9175
9175
|
* @private
|
|
9176
9176
|
*/
|
|
9177
9177
|
_compressFile(src, dest, callback) {
|
|
9178
|
-
|
|
9178
|
+
fs40.access(src, fs40.F_OK, (err2) => {
|
|
9179
9179
|
if (err2) {
|
|
9180
9180
|
return callback();
|
|
9181
9181
|
}
|
|
9182
9182
|
var gzip = zlib3.createGzip();
|
|
9183
|
-
var inp =
|
|
9184
|
-
var out =
|
|
9183
|
+
var inp = fs40.createReadStream(src);
|
|
9184
|
+
var out = fs40.createWriteStream(dest);
|
|
9185
9185
|
out.on("finish", () => {
|
|
9186
|
-
|
|
9186
|
+
fs40.unlink(src, callback);
|
|
9187
9187
|
});
|
|
9188
9188
|
inp.pipe(gzip).pipe(out);
|
|
9189
9189
|
});
|
|
9190
9190
|
}
|
|
9191
9191
|
_createLogDirIfNotExist(dirPath) {
|
|
9192
|
-
if (!
|
|
9193
|
-
|
|
9192
|
+
if (!fs40.existsSync(dirPath)) {
|
|
9193
|
+
fs40.mkdirSync(dirPath, { recursive: true });
|
|
9194
9194
|
}
|
|
9195
9195
|
}
|
|
9196
9196
|
};
|
|
@@ -11761,7 +11761,7 @@ var CONSTANTS, Constants_default;
|
|
|
11761
11761
|
var init_Constants = __esm({
|
|
11762
11762
|
"../../packages/constants/src/Constants.ts"() {
|
|
11763
11763
|
CONSTANTS = {
|
|
11764
|
-
cliVersion: "2.
|
|
11764
|
+
cliVersion: "2.4.0",
|
|
11765
11765
|
backendVersion: 1,
|
|
11766
11766
|
backendPort: 5088,
|
|
11767
11767
|
workerVersion: 2,
|
|
@@ -11890,7 +11890,25 @@ var init_Constants = __esm({
|
|
|
11890
11890
|
* Rows a sample may ask for out of the pipeline. A sample is a look at the data, never an
|
|
11891
11891
|
* export of it — `remora sample -s` and the preview's Data tab are both held to this.
|
|
11892
11892
|
*/
|
|
11893
|
-
MAX_SAMPLE_ROWS: 100
|
|
11893
|
+
MAX_SAMPLE_ROWS: 100,
|
|
11894
|
+
/**
|
|
11895
|
+
* Values held in memory while one parquet row group is encoded. A parquet writer buffers a
|
|
11896
|
+
* whole row group before it can compress and flush it, so this — not the dataset size — is
|
|
11897
|
+
* what bounds the export's peak memory. Budgeted in CELLS rather than rows because the two
|
|
11898
|
+
* differ by three orders of magnitude across consumers: 100,000 rows of a 4-column consumer
|
|
11899
|
+
* is 400k values, of a 1,300-field one it is 130 million.
|
|
11900
|
+
*/
|
|
11901
|
+
PARQUET_ROW_GROUP_CELLS: 1e6,
|
|
11902
|
+
/**
|
|
11903
|
+
* Floor on the derived row count, so a consumer with hundreds of columns still writes row
|
|
11904
|
+
* groups big enough to compress and to read back without a seek per handful of rows.
|
|
11905
|
+
*/
|
|
11906
|
+
PARQUET_MIN_ROW_GROUP_ROWS: 1e3,
|
|
11907
|
+
/**
|
|
11908
|
+
* Ceiling on the derived row count, so a one-column consumer does not buffer a million rows
|
|
11909
|
+
* to save a few row-group footers.
|
|
11910
|
+
*/
|
|
11911
|
+
PARQUET_MAX_ROW_GROUP_ROWS: 1e5
|
|
11894
11912
|
}
|
|
11895
11913
|
};
|
|
11896
11914
|
Constants_default = CONSTANTS;
|
|
@@ -11984,11 +12002,26 @@ var init_ExecutorScope = __esm({
|
|
|
11984
12002
|
import_path2 = __toESM(require("path"), 1);
|
|
11985
12003
|
import_fs2 = __toESM(require("fs"), 1);
|
|
11986
12004
|
import_promises = __toESM(require("fs/promises"), 1);
|
|
12005
|
+
init_src();
|
|
11987
12006
|
init_src3();
|
|
11988
12007
|
ExecutorScopeClass = class {
|
|
11989
12008
|
constructor() {
|
|
11990
12009
|
this.WORKERS_FOLDER = "workers";
|
|
11991
12010
|
this.PRODUCERS_FOLDER = "producers";
|
|
12011
|
+
/**
|
|
12012
|
+
* The scope folders THIS process opened and has not cleared. The only list `deepClear` may
|
|
12013
|
+
* delete from: two remora processes share one `.temp`, so a listing cannot tell a crashed run's
|
|
12014
|
+
* leftovers from another process's live run.
|
|
12015
|
+
*/
|
|
12016
|
+
this._ownedScopes = /* @__PURE__ */ new Set();
|
|
12017
|
+
/** Registers a scope as this process's, so a crash on the way out clears it. */
|
|
12018
|
+
this.open = (scope) => {
|
|
12019
|
+
Affirm_default(scope?.folder, "Invalid executor scope");
|
|
12020
|
+
this._ownedScopes.add(scope.folder);
|
|
12021
|
+
return scope;
|
|
12022
|
+
};
|
|
12023
|
+
/** The scopes this process still holds. Empty once every run it started has cleaned up. */
|
|
12024
|
+
this.getOwnedScopes = () => [...this._ownedScopes];
|
|
11992
12025
|
this.getBasePath = () => import_path2.default.join(Constants_default.defaults.REMORA_PATH, Constants_default.defaults.PRODUCER_TEMP_FOLDER);
|
|
11993
12026
|
this.getWorkerPath = (scope, workerId) => {
|
|
11994
12027
|
return import_path2.default.join(
|
|
@@ -11999,6 +12032,13 @@ var init_ExecutorScope = __esm({
|
|
|
11999
12032
|
`${workerId}.dataset`
|
|
12000
12033
|
);
|
|
12001
12034
|
};
|
|
12035
|
+
/**
|
|
12036
|
+
* Where a producer's source is staged before any worker reads it: a CSV extract WITH a header,
|
|
12037
|
+
* written by whichever driver read the source.
|
|
12038
|
+
*
|
|
12039
|
+
* `.extract`, not `.dataset`: the two are different formats, and one extension meaning both is
|
|
12040
|
+
* how a reader ends up applying the internal positional rules to a file that follows none.
|
|
12041
|
+
*/
|
|
12002
12042
|
this.getProducerPath = (scope, producer, sourceFileKey) => {
|
|
12003
12043
|
return import_path2.default.join(
|
|
12004
12044
|
this.getBasePath(),
|
|
@@ -12006,7 +12046,7 @@ var init_ExecutorScope = __esm({
|
|
|
12006
12046
|
scope.folder,
|
|
12007
12047
|
this.PRODUCERS_FOLDER,
|
|
12008
12048
|
producer.name,
|
|
12009
|
-
`${sourceFileKey}.
|
|
12049
|
+
`${sourceFileKey}.extract`
|
|
12010
12050
|
);
|
|
12011
12051
|
};
|
|
12012
12052
|
this.getMainPath = (scope) => {
|
|
@@ -12042,16 +12082,32 @@ var init_ExecutorScope = __esm({
|
|
|
12042
12082
|
if (import_fs2.default.existsSync(scopePath)) {
|
|
12043
12083
|
await import_promises.default.rm(scopePath, { recursive: true, force: true });
|
|
12044
12084
|
}
|
|
12085
|
+
this._ownedScopes.delete(scope.folder);
|
|
12045
12086
|
};
|
|
12087
|
+
/**
|
|
12088
|
+
* Clears what this process left open, on the way out. Synchronous: its one caller is the `exit`
|
|
12089
|
+
* handler. A run that finished cleared its own scope, so a clean exit finds nothing to do.
|
|
12090
|
+
*/
|
|
12046
12091
|
this.deepClear = () => {
|
|
12047
12092
|
const basePath = this.getBasePath();
|
|
12048
|
-
const
|
|
12049
|
-
for (const scopeFolder of openScopes) {
|
|
12093
|
+
for (const scopeFolder of this._ownedScopes) {
|
|
12050
12094
|
const scopePath = import_path2.default.join(basePath, scopeFolder);
|
|
12051
12095
|
if (import_fs2.default.existsSync(scopePath)) {
|
|
12052
12096
|
import_fs2.default.rmSync(scopePath, { recursive: true, force: true });
|
|
12053
12097
|
}
|
|
12054
12098
|
}
|
|
12099
|
+
this._ownedScopes.clear();
|
|
12100
|
+
};
|
|
12101
|
+
/**
|
|
12102
|
+
* Every scope folder on disk, whoever opened it. Answers "what is in .temp?", not "what is mine
|
|
12103
|
+
* to delete" — nothing may be removed on the strength of it (see `getOwnedScopes`). The parallel
|
|
12104
|
+
* canary samples it to watch concurrent runs come and go.
|
|
12105
|
+
*/
|
|
12106
|
+
this.getOpenScopes = () => {
|
|
12107
|
+
const basePath = this.getBasePath();
|
|
12108
|
+
if (!import_fs2.default.existsSync(basePath))
|
|
12109
|
+
return [];
|
|
12110
|
+
return import_fs2.default.readdirSync(basePath, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name).filter((folder) => folder !== "logs" && folder !== "usage");
|
|
12055
12111
|
};
|
|
12056
12112
|
this.ensurePath = (fileUri) => {
|
|
12057
12113
|
const dir = import_path2.default.dirname(fileUri);
|
|
@@ -12060,12 +12116,6 @@ var init_ExecutorScope = __esm({
|
|
|
12060
12116
|
if (!import_fs2.default.existsSync(fileUri))
|
|
12061
12117
|
import_fs2.default.writeFileSync(fileUri, "");
|
|
12062
12118
|
};
|
|
12063
|
-
this.getOpenScopes = () => {
|
|
12064
|
-
const basePath = this.getBasePath();
|
|
12065
|
-
if (!import_fs2.default.existsSync(basePath))
|
|
12066
|
-
return [];
|
|
12067
|
-
return import_fs2.default.readdirSync(basePath, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name).filter((folder) => folder !== "logs" && folder !== "usage");
|
|
12068
|
-
};
|
|
12069
12119
|
}
|
|
12070
12120
|
};
|
|
12071
12121
|
ExecutorScope = new ExecutorScopeClass();
|
|
@@ -12119,8 +12169,7 @@ var init_ProcessShutdownManager = __esm({
|
|
|
12119
12169
|
if (this._cleaned)
|
|
12120
12170
|
return;
|
|
12121
12171
|
this._cleaned = true;
|
|
12122
|
-
const
|
|
12123
|
-
const scopeCount = openScopes.length;
|
|
12172
|
+
const scopeCount = ExecutorScope_default.getOwnedScopes().length;
|
|
12124
12173
|
const shutdownState = this._shutdownState;
|
|
12125
12174
|
const shutdownDescription = `${shutdownState?.type ?? "intentional"} shutdown (${shutdownState?.reason ?? `exit:${code}`})`;
|
|
12126
12175
|
if (scopeCount === 0) {
|
|
@@ -12532,7 +12581,7 @@ var init_ResourcesUtils = __esm({
|
|
|
12532
12581
|
Affirm_default(subConsumer, `No producer found with name "${cProd.name}"`);
|
|
12533
12582
|
return this.getConsumerOutputColumns(subConsumer);
|
|
12534
12583
|
} else {
|
|
12535
|
-
|
|
12584
|
+
return producer.dimensions.map((x2) => ({
|
|
12536
12585
|
consumerAlias: null,
|
|
12537
12586
|
consumerKey: null,
|
|
12538
12587
|
nameInProducer: x2.name,
|
|
@@ -12540,15 +12589,6 @@ var init_ResourcesUtils = __esm({
|
|
|
12540
12589
|
dimension: x2,
|
|
12541
12590
|
owner: cProd.name
|
|
12542
12591
|
}));
|
|
12543
|
-
const meas = producer.measures?.map((x2) => ({
|
|
12544
|
-
consumerAlias: null,
|
|
12545
|
-
consumerKey: null,
|
|
12546
|
-
nameInProducer: x2.name,
|
|
12547
|
-
aliasInProducer: x2.name,
|
|
12548
|
-
measure: x2,
|
|
12549
|
-
owner: cProd.name
|
|
12550
|
-
})) ?? [];
|
|
12551
|
-
return [...dims, ...meas];
|
|
12552
12592
|
}
|
|
12553
12593
|
});
|
|
12554
12594
|
};
|
|
@@ -12608,7 +12648,6 @@ var init_ResourcesUtils = __esm({
|
|
|
12608
12648
|
nameInProducer: col.nameInProducer,
|
|
12609
12649
|
aliasInProducer: col.nameInProducer,
|
|
12610
12650
|
dimension: col.dimension,
|
|
12611
|
-
measure: col.measure,
|
|
12612
12651
|
owner: consumer.name
|
|
12613
12652
|
});
|
|
12614
12653
|
}
|
|
@@ -12622,7 +12661,6 @@ var init_ResourcesUtils = __esm({
|
|
|
12622
12661
|
nameInProducer: match2.name,
|
|
12623
12662
|
aliasInProducer: match2.name,
|
|
12624
12663
|
dimension: match2.column.dimension,
|
|
12625
|
-
measure: match2.column.measure,
|
|
12626
12664
|
owner: consumer.name
|
|
12627
12665
|
});
|
|
12628
12666
|
}
|
|
@@ -12636,7 +12674,6 @@ var init_ResourcesUtils = __esm({
|
|
|
12636
12674
|
nameInProducer: finalKey,
|
|
12637
12675
|
aliasInProducer: finalKey,
|
|
12638
12676
|
dimension: match?.dimension ?? { name: finalKey, type: "string" },
|
|
12639
|
-
measure: match?.measure,
|
|
12640
12677
|
owner: consumer.name
|
|
12641
12678
|
});
|
|
12642
12679
|
}
|
|
@@ -13088,11 +13125,8 @@ var init_Validator = __esm({
|
|
|
13088
13125
|
return errors2;
|
|
13089
13126
|
};
|
|
13090
13127
|
errors.push(...validateTransformations(consumer.fields));
|
|
13091
|
-
|
|
13092
|
-
|
|
13093
|
-
const duplicatesTypes = Algo_default.uniq(duplicatesOutputs.map((x2) => x2.format));
|
|
13094
|
-
errors.push(`There are outputs with the same type. (duplicates type: ${duplicatesTypes.join(" and ")})`);
|
|
13095
|
-
}
|
|
13128
|
+
if ((consumer.outputs ?? []).length > 1)
|
|
13129
|
+
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.`);
|
|
13096
13130
|
for (const output of consumer.outputs) {
|
|
13097
13131
|
const format4 = output.format.toUpperCase();
|
|
13098
13132
|
if (format4 === "CSV" || format4 === "JSON" || format4 === "PARQUET" || format4 === "TXT") {
|
|
@@ -13190,47 +13224,41 @@ var init_Validator = __esm({
|
|
|
13190
13224
|
const missingKeys = fieldKeys.includes("*") ? [] : keys.filter((key) => !fieldKeys.includes(key));
|
|
13191
13225
|
if (missingKeys.length > 0)
|
|
13192
13226
|
errors.push(`"split.by" references key(s) "${missingKeys.join(", ")}" that are not present in the consumer "${consumer.name}".`);
|
|
13193
|
-
const
|
|
13194
|
-
if (
|
|
13227
|
+
const format4 = consumer.outputs?.[0]?.format.toUpperCase();
|
|
13228
|
+
if (!positionalFormats.includes(format4)) return errors;
|
|
13195
13229
|
const hiddenKeys = keys.filter((key) => (consumer.fields ?? []).some((x2) => (x2.alias ?? x2.key) === key && x2.hidden));
|
|
13196
13230
|
if (hiddenKeys.length > 0)
|
|
13197
|
-
errors.push(`"split.by" of consumer "${consumer.name}" references hidden field(s) "${hiddenKeys.join(", ")}", which the ${
|
|
13231
|
+
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.`);
|
|
13198
13232
|
return errors;
|
|
13199
13233
|
};
|
|
13200
13234
|
/**
|
|
13201
|
-
* Checks `omitNull`, which drops a record's null fields on export. It only means anything where
|
|
13202
|
-
* record carries its own field names
|
|
13203
|
-
*
|
|
13204
|
-
* column alignment. Both cases are rejected rather than ignored.
|
|
13235
|
+
* Checks `omitNull`, which drops a record's null fields on export. It only means anything where
|
|
13236
|
+
* a record carries its own field names: leaving a positional column out shifts every column
|
|
13237
|
+
* after it, so a positional output declaring it is rejected rather than ignored.
|
|
13205
13238
|
*/
|
|
13206
13239
|
this.validateOmitNull = (consumer) => {
|
|
13207
13240
|
const errors = [];
|
|
13208
13241
|
const positionalFormats = ["CSV", "TXT", "PARQUET"];
|
|
13209
13242
|
for (const output of (consumer.outputs ?? []).filter((x2) => x2.omitNull === true)) {
|
|
13210
13243
|
const format4 = output.format.toUpperCase();
|
|
13211
|
-
if (positionalFormats.includes(format4))
|
|
13244
|
+
if (positionalFormats.includes(format4))
|
|
13212
13245
|
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.`);
|
|
13213
|
-
continue;
|
|
13214
|
-
}
|
|
13215
|
-
const positional = (consumer.outputs ?? []).filter((x2) => positionalFormats.includes(x2.format.toUpperCase()));
|
|
13216
|
-
if (positional.length > 0)
|
|
13217
|
-
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.`);
|
|
13218
13246
|
}
|
|
13219
13247
|
return errors;
|
|
13220
13248
|
};
|
|
13221
13249
|
/**
|
|
13222
13250
|
* Checks `header`, which writes the field names as the first line of an exported file. A JSON
|
|
13223
|
-
* record carries its own field names
|
|
13224
|
-
* nothing there — rejected rather than ignored, so a config that
|
|
13225
|
-
* one.
|
|
13251
|
+
* record carries its own field names, a parquet file carries its own schema, and an API response
|
|
13252
|
+
* is not a file, so the flag means nothing there — rejected rather than ignored, so a config that
|
|
13253
|
+
* asks for a header always gets one.
|
|
13226
13254
|
*/
|
|
13227
13255
|
this.validateHeader = (consumer) => {
|
|
13228
13256
|
const errors = [];
|
|
13229
|
-
const
|
|
13257
|
+
const headerFormats = ["CSV", "TXT"];
|
|
13230
13258
|
for (const output of (consumer.outputs ?? []).filter((x2) => Algo_default.hasVal(x2.header))) {
|
|
13231
13259
|
const format4 = output.format.toUpperCase();
|
|
13232
|
-
if (!
|
|
13233
|
-
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.`);
|
|
13260
|
+
if (!headerFormats.includes(format4))
|
|
13261
|
+
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"}.`);
|
|
13234
13262
|
}
|
|
13235
13263
|
return errors;
|
|
13236
13264
|
};
|
|
@@ -33502,6 +33530,96 @@ var init_CSVParser = __esm({
|
|
|
33502
33530
|
}
|
|
33503
33531
|
});
|
|
33504
33532
|
|
|
33533
|
+
// ../../packages/parsing/src/DatasetCodec.ts
|
|
33534
|
+
var NULL_FIELD, NEEDS_ESCAPE, DatasetCodecClass, DatasetCodec, DatasetCodec_default;
|
|
33535
|
+
var init_DatasetCodec = __esm({
|
|
33536
|
+
"../../packages/parsing/src/DatasetCodec.ts"() {
|
|
33537
|
+
init_src();
|
|
33538
|
+
NULL_FIELD = "";
|
|
33539
|
+
NEEDS_ESCAPE = /["\n\r]/;
|
|
33540
|
+
DatasetCodecClass = class {
|
|
33541
|
+
constructor() {
|
|
33542
|
+
/**
|
|
33543
|
+
* One value as an internal positional field: quoted, with `"`, `\n` and `\r` escaped - or the
|
|
33544
|
+
* unquoted null marker when there is no value.
|
|
33545
|
+
*/
|
|
33546
|
+
this.encodeField = (value) => {
|
|
33547
|
+
if (value === null || value === void 0) return NULL_FIELD;
|
|
33548
|
+
const text = typeof value === "string" ? value : String(value);
|
|
33549
|
+
if (!NEEDS_ESCAPE.test(text)) return `"${text}"`;
|
|
33550
|
+
return `"${text.replaceAll('"', '""').replaceAll("\r", "\\r").replaceAll("\n", "\\n")}"`;
|
|
33551
|
+
};
|
|
33552
|
+
/** One record as an internal positional line. The values are already in the schema's order. */
|
|
33553
|
+
this.encodeRow = (values, delimiter = ",") => {
|
|
33554
|
+
Affirm_default(values, "Invalid values");
|
|
33555
|
+
Affirm_default(delimiter, "Invalid delimiter");
|
|
33556
|
+
let row = "";
|
|
33557
|
+
for (let index = 0; index < values.length; index++) {
|
|
33558
|
+
if (index > 0)
|
|
33559
|
+
row += delimiter;
|
|
33560
|
+
row += this.encodeField(values[index]);
|
|
33561
|
+
}
|
|
33562
|
+
return row;
|
|
33563
|
+
};
|
|
33564
|
+
/**
|
|
33565
|
+
* One internal positional line split back into its raw fields, with a null still a null. The
|
|
33566
|
+
* array may be SHORTER than the schema's column list; a reader treats a missing entry as null.
|
|
33567
|
+
*/
|
|
33568
|
+
this.decodeRow = (line, delimiter = ",") => {
|
|
33569
|
+
Affirm_default(delimiter, "Invalid delimiter");
|
|
33570
|
+
const fields = [];
|
|
33571
|
+
const length = line?.length ?? 0;
|
|
33572
|
+
const delimiterCode = delimiter.charCodeAt(0);
|
|
33573
|
+
let fieldStart = 0;
|
|
33574
|
+
let fieldEnd = 0;
|
|
33575
|
+
let inQuotes = false;
|
|
33576
|
+
let hasQuotes = false;
|
|
33577
|
+
let index = 0;
|
|
33578
|
+
while (index < length) {
|
|
33579
|
+
const char = line.charCodeAt(index);
|
|
33580
|
+
if (char === 34) {
|
|
33581
|
+
if (!inQuotes) {
|
|
33582
|
+
inQuotes = true;
|
|
33583
|
+
hasQuotes = true;
|
|
33584
|
+
fieldStart = index + 1;
|
|
33585
|
+
} else if (line.charCodeAt(index + 1) === 34) {
|
|
33586
|
+
index++;
|
|
33587
|
+
} else {
|
|
33588
|
+
inQuotes = false;
|
|
33589
|
+
fieldEnd = index;
|
|
33590
|
+
}
|
|
33591
|
+
} else if (char === delimiterCode && !inQuotes) {
|
|
33592
|
+
fields.push(this._field(line, fieldStart, fieldEnd, hasQuotes));
|
|
33593
|
+
fieldStart = index + 1;
|
|
33594
|
+
fieldEnd = 0;
|
|
33595
|
+
hasQuotes = false;
|
|
33596
|
+
}
|
|
33597
|
+
index++;
|
|
33598
|
+
}
|
|
33599
|
+
fields.push(this._field(line, fieldStart, fieldEnd, hasQuotes));
|
|
33600
|
+
return fields;
|
|
33601
|
+
};
|
|
33602
|
+
/**
|
|
33603
|
+
* Re-encodes an internal line with another delimiter, which is what a `TXT` export is. Not via
|
|
33604
|
+
* `CSVParser`: a plain parse reads both the null marker and `""` as the empty string.
|
|
33605
|
+
*/
|
|
33606
|
+
this.redelimit = (line, delimiter) => {
|
|
33607
|
+
Affirm_default(delimiter, "Invalid delimiter");
|
|
33608
|
+
return this.encodeRow(this.decodeRow(line), delimiter);
|
|
33609
|
+
};
|
|
33610
|
+
/** One field's text, or null for the unquoted marker. Only a quoted token carries a value. */
|
|
33611
|
+
this._field = (line, start, end, hasQuotes) => {
|
|
33612
|
+
if (!hasQuotes) return null;
|
|
33613
|
+
const raw = line.slice(start, end);
|
|
33614
|
+
return raw.indexOf('"') === -1 ? raw : raw.replaceAll('""', '"');
|
|
33615
|
+
};
|
|
33616
|
+
}
|
|
33617
|
+
};
|
|
33618
|
+
DatasetCodec = new DatasetCodecClass();
|
|
33619
|
+
DatasetCodec_default = DatasetCodec;
|
|
33620
|
+
}
|
|
33621
|
+
});
|
|
33622
|
+
|
|
33505
33623
|
// ../../packages/parsing/src/FixedWidthParser.ts
|
|
33506
33624
|
var FixedWidthParserClass, FixedWidthParser, FixedWidthParser_default;
|
|
33507
33625
|
var init_FixedWidthParser = __esm({
|
|
@@ -33714,7 +33832,7 @@ var require_pend = __commonJS({
|
|
|
33714
33832
|
// ../../node_modules/yauzl/fd-slicer.js
|
|
33715
33833
|
var require_fd_slicer = __commonJS({
|
|
33716
33834
|
"../../node_modules/yauzl/fd-slicer.js"(exports2) {
|
|
33717
|
-
var
|
|
33835
|
+
var fs40 = require("fs");
|
|
33718
33836
|
var util = require("util");
|
|
33719
33837
|
var stream = require("stream");
|
|
33720
33838
|
var Readable10 = stream.Readable;
|
|
@@ -33734,7 +33852,7 @@ var require_fd_slicer = __commonJS({
|
|
|
33734
33852
|
FdSlicer.prototype.read = function(buffer, offset, length, position, callback) {
|
|
33735
33853
|
var self2 = this;
|
|
33736
33854
|
self2.pend.go(function(cb) {
|
|
33737
|
-
|
|
33855
|
+
fs40.read(self2.fd, buffer, offset, length, position, function(err2, bytesRead, buffer2) {
|
|
33738
33856
|
cb();
|
|
33739
33857
|
callback(err2, bytesRead, buffer2);
|
|
33740
33858
|
});
|
|
@@ -33751,7 +33869,7 @@ var require_fd_slicer = __commonJS({
|
|
|
33751
33869
|
self2.refCount -= 1;
|
|
33752
33870
|
if (self2.refCount < 0) throw new Error("invalid unref");
|
|
33753
33871
|
if (self2.refCount > 0) return;
|
|
33754
|
-
|
|
33872
|
+
fs40.close(self2.fd, onCloseDone);
|
|
33755
33873
|
function onCloseDone(err2) {
|
|
33756
33874
|
if (err2) {
|
|
33757
33875
|
self2.emit("error", err2);
|
|
@@ -33783,7 +33901,7 @@ var require_fd_slicer = __commonJS({
|
|
|
33783
33901
|
}
|
|
33784
33902
|
self2.context.pend.go(function(cb) {
|
|
33785
33903
|
var buffer = Buffer.allocUnsafe(toRead);
|
|
33786
|
-
|
|
33904
|
+
fs40.read(self2.context.fd, buffer, 0, toRead, self2.pos, function(err2, bytesRead) {
|
|
33787
33905
|
if (err2) {
|
|
33788
33906
|
self2.destroy(err2);
|
|
33789
33907
|
} else if (bytesRead === 0) {
|
|
@@ -34143,7 +34261,7 @@ var require_crc32 = __commonJS({
|
|
|
34143
34261
|
// ../../node_modules/yauzl/index.js
|
|
34144
34262
|
var require_yauzl = __commonJS({
|
|
34145
34263
|
"../../node_modules/yauzl/index.js"(exports2) {
|
|
34146
|
-
var
|
|
34264
|
+
var fs40 = require("fs");
|
|
34147
34265
|
var zlib3 = require("zlib");
|
|
34148
34266
|
var fd_slicer = require_fd_slicer();
|
|
34149
34267
|
var util = require("util");
|
|
@@ -34212,10 +34330,10 @@ var require_yauzl = __commonJS({
|
|
|
34212
34330
|
if (options2.validateEntrySizes == null) options2.validateEntrySizes = true;
|
|
34213
34331
|
if (options2.strictFileNames == null) options2.strictFileNames = false;
|
|
34214
34332
|
if (callback == null) callback = defaultCallback;
|
|
34215
|
-
|
|
34333
|
+
fs40.open(path30, "r", function(err2, fd) {
|
|
34216
34334
|
if (err2) return callback(err2);
|
|
34217
34335
|
fromFd(fd, options2, function(err3, zipfile) {
|
|
34218
|
-
if (err3)
|
|
34336
|
+
if (err3) fs40.close(fd, defaultCallback);
|
|
34219
34337
|
callback(err3, zipfile);
|
|
34220
34338
|
});
|
|
34221
34339
|
});
|
|
@@ -34232,7 +34350,7 @@ var require_yauzl = __commonJS({
|
|
|
34232
34350
|
if (options2.validateEntrySizes == null) options2.validateEntrySizes = true;
|
|
34233
34351
|
if (options2.strictFileNames == null) options2.strictFileNames = false;
|
|
34234
34352
|
if (callback == null) callback = defaultCallback;
|
|
34235
|
-
|
|
34353
|
+
fs40.fstat(fd, function(err2, stats) {
|
|
34236
34354
|
if (err2) return callback(err2);
|
|
34237
34355
|
var reader = new fd_slicer.FdSlicer(fd);
|
|
34238
34356
|
fromRandomAccessReader(reader, stats.size, options2, callback);
|
|
@@ -38311,6 +38429,7 @@ var init_XMLParser = __esm({
|
|
|
38311
38429
|
var init_src6 = __esm({
|
|
38312
38430
|
"../../packages/parsing/src/index.ts"() {
|
|
38313
38431
|
init_CSVParser();
|
|
38432
|
+
init_DatasetCodec();
|
|
38314
38433
|
init_FixedWidthParser();
|
|
38315
38434
|
init_ParseCompression();
|
|
38316
38435
|
init_ParseHelper();
|
|
@@ -47818,7 +47937,7 @@ var init_ProducerManager = __esm({
|
|
|
47818
47937
|
});
|
|
47819
47938
|
|
|
47820
47939
|
// ../../packages/engines/src/transform/TypeCaster.ts
|
|
47821
|
-
var import_dayjs, import_customParseFormat, import_utc, DAY_OF_YEAR_TOKEN, FORMAT_ONLY_TOKENS, DATE_PARSE_TOKENS, TYPE_DEFAULTS, TypeCasterClass, TypeCaster, TypeCaster_default;
|
|
47940
|
+
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;
|
|
47822
47941
|
var init_TypeCaster = __esm({
|
|
47823
47942
|
"../../packages/engines/src/transform/TypeCaster.ts"() {
|
|
47824
47943
|
init_src();
|
|
@@ -47839,6 +47958,7 @@ var init_TypeCaster = __esm({
|
|
|
47839
47958
|
{ token: "mm", width: 2 },
|
|
47840
47959
|
{ token: "ss", width: 2 }
|
|
47841
47960
|
];
|
|
47961
|
+
TRUE_VALUES = /^(1|true|yes|y|t)$/i;
|
|
47842
47962
|
TYPE_DEFAULTS = {
|
|
47843
47963
|
string: "",
|
|
47844
47964
|
number: 0,
|
|
@@ -47848,7 +47968,8 @@ var init_TypeCaster = __esm({
|
|
|
47848
47968
|
TypeCasterClass = class {
|
|
47849
47969
|
constructor() {
|
|
47850
47970
|
/**
|
|
47851
|
-
* Casts a value for a DECLARED
|
|
47971
|
+
* Casts a value for a DECLARED field: the type cast, plus what a MISSING value means for it. The
|
|
47972
|
+
* field is a producer dimension on the read path and a dataset column on the re-read path.
|
|
47852
47973
|
*
|
|
47853
47974
|
* Missing is null/undefined, and — for every type but `string` — the empty string, which is how
|
|
47854
47975
|
* a NULL arrives from every source that stages through CSV (SQL, Delta Share, CSV files) and is
|
|
@@ -47880,6 +48001,7 @@ var init_TypeCaster = __esm({
|
|
|
47880
48001
|
switch (type) {
|
|
47881
48002
|
case "boolean": {
|
|
47882
48003
|
if (typeof value === "boolean") return value;
|
|
48004
|
+
if (typeof value === "string") return TRUE_VALUES.test(value.trim());
|
|
47883
48005
|
return Boolean(value);
|
|
47884
48006
|
}
|
|
47885
48007
|
case "datetime":
|
|
@@ -48018,6 +48140,7 @@ var init_TypeCaster = __esm({
|
|
|
48018
48140
|
var LineParserClass, LineParser, LineParser_default;
|
|
48019
48141
|
var init_LineParser = __esm({
|
|
48020
48142
|
"../../packages/engines/src/parsing/LineParser.ts"() {
|
|
48143
|
+
init_src();
|
|
48021
48144
|
init_TypeCaster();
|
|
48022
48145
|
init_src6();
|
|
48023
48146
|
LineParserClass = class {
|
|
@@ -48110,22 +48233,23 @@ var init_LineParser = __esm({
|
|
|
48110
48233
|
return record;
|
|
48111
48234
|
};
|
|
48112
48235
|
/**
|
|
48113
|
-
*
|
|
48114
|
-
|
|
48115
|
-
|
|
48116
|
-
const parts = CSVParser_default.parseRow(line, delimiter ?? ",");
|
|
48117
|
-
const record = {};
|
|
48118
|
-
for (const [index, field] of fields.entries()) {
|
|
48119
|
-
const fieldKey = field.finalKey;
|
|
48120
|
-
record[fieldKey] = field.dimension ? TypeCaster_default.castDimension(parts[index], field.dimension) : TypeCaster_default.cast(parts[index], "string");
|
|
48121
|
-
}
|
|
48122
|
-
return record;
|
|
48123
|
-
};
|
|
48124
|
-
/**
|
|
48125
|
-
* Used ONLY to parse internal records (inside the .dataset) since I know they are already prepared in the correct way
|
|
48236
|
+
* The parser of one staged `.dataset`, compiled once from the schema that file was written with.
|
|
48237
|
+
* The schema is the ONLY description of the file - it has no header - and there is deliberately
|
|
48238
|
+
* no overload taking a consumer's field list, which cannot know about a shape a pass changed.
|
|
48126
48239
|
*/
|
|
48127
|
-
this.
|
|
48128
|
-
|
|
48240
|
+
this.datasetParser = (dsSchema) => {
|
|
48241
|
+
Affirm_default(dsSchema, "Invalid dataset schema");
|
|
48242
|
+
Affirm_default.hasItems(dsSchema.columns, "The dataset schema declares no column to read");
|
|
48243
|
+
if (dsSchema.encoding === "JSON")
|
|
48244
|
+
return (line) => JSON.parse(line);
|
|
48245
|
+
const { columns } = dsSchema;
|
|
48246
|
+
return (line) => {
|
|
48247
|
+
const parts = DatasetCodec_default.decodeRow(line);
|
|
48248
|
+
const record = {};
|
|
48249
|
+
for (let index = 0; index < columns.length; index++)
|
|
48250
|
+
record[columns[index].name] = TypeCaster_default.castDimension(parts[index], columns[index]);
|
|
48251
|
+
return record;
|
|
48252
|
+
};
|
|
48129
48253
|
};
|
|
48130
48254
|
}
|
|
48131
48255
|
};
|
|
@@ -48242,7 +48366,7 @@ var init_ProducerEngine = __esm({
|
|
|
48242
48366
|
let effectiveProducer = producer;
|
|
48243
48367
|
let effectiveSource = source;
|
|
48244
48368
|
if (compressionType) {
|
|
48245
|
-
scope = { id: (0, import_crypto7.randomUUID)(), folder: `sample-${(0, import_crypto7.randomUUID)()}`, workersId: [] };
|
|
48369
|
+
scope = ExecutorScope_default.open({ id: (0, import_crypto7.randomUUID)(), folder: `sample-${(0, import_crypto7.randomUUID)()}`, workersId: [] });
|
|
48246
48370
|
const driver = await DriverFactory_default.instantiateSource(source);
|
|
48247
48371
|
const readyResult = await driver.ready({ producer, scope });
|
|
48248
48372
|
const firstFile = readyResult.files[0].fullUri;
|
|
@@ -48252,7 +48376,7 @@ var init_ProducerEngine = __esm({
|
|
|
48252
48376
|
...producer,
|
|
48253
48377
|
settings: {
|
|
48254
48378
|
...producer.settings,
|
|
48255
|
-
fileKey: readyResult.files.length > 1 ? "%.
|
|
48379
|
+
fileKey: readyResult.files.length > 1 ? "%.extract" : import_path18.default.basename(firstFile),
|
|
48256
48380
|
compressionType: void 0
|
|
48257
48381
|
}
|
|
48258
48382
|
};
|
|
@@ -51506,8 +51630,7 @@ var init_ConsumerManager = __esm({
|
|
|
51506
51630
|
from: x2.owner
|
|
51507
51631
|
},
|
|
51508
51632
|
finalKey: x2.nameInProducer,
|
|
51509
|
-
dimension: x2.dimension
|
|
51510
|
-
measure: x2.measure
|
|
51633
|
+
dimension: x2.dimension
|
|
51511
51634
|
})));
|
|
51512
51635
|
} else {
|
|
51513
51636
|
const from = field.cField.from ?? (consumer.producers.length === 1 ? consumer.producers[0].name : null);
|
|
@@ -51519,8 +51642,7 @@ var init_ConsumerManager = __esm({
|
|
|
51519
51642
|
from: col.owner
|
|
51520
51643
|
},
|
|
51521
51644
|
finalKey: col.nameInProducer,
|
|
51522
|
-
dimension: col.dimension
|
|
51523
|
-
measure: col.measure
|
|
51645
|
+
dimension: col.dimension
|
|
51524
51646
|
});
|
|
51525
51647
|
});
|
|
51526
51648
|
}
|
|
@@ -51535,13 +51657,12 @@ var init_ConsumerManager = __esm({
|
|
|
51535
51657
|
from: field.cField.from ?? match.column.owner
|
|
51536
51658
|
},
|
|
51537
51659
|
finalKey: match.name,
|
|
51538
|
-
dimension: match.column.dimension
|
|
51539
|
-
measure: match.column.measure
|
|
51660
|
+
dimension: match.column.dimension
|
|
51540
51661
|
})));
|
|
51541
51662
|
} else {
|
|
51542
51663
|
const col = ConsumerManager.searchFieldInColumns(field.cField, availableColumns, consumer);
|
|
51543
51664
|
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(", ")}")`);
|
|
51544
|
-
expandedFields.push({ ...field, dimension: col.dimension
|
|
51665
|
+
expandedFields.push({ ...field, dimension: col.dimension });
|
|
51545
51666
|
}
|
|
51546
51667
|
return expandedFields;
|
|
51547
51668
|
};
|
|
@@ -52603,10 +52724,9 @@ var init_FieldLineage = __esm({
|
|
|
52603
52724
|
this._fromProducer = (producer, field) => {
|
|
52604
52725
|
const node = LineageEngine_default.nodeId("producer", producer.name);
|
|
52605
52726
|
const dimension = (producer.dimensions ?? []).find((x2) => x2.name === field.key);
|
|
52606
|
-
const
|
|
52607
|
-
const applied = dimension?.alias ? [`source column "${dimension.alias}"`] : measure ? ["measure"] : [];
|
|
52727
|
+
const applied = dimension?.alias ? [`source column "${dimension.alias}"`] : [];
|
|
52608
52728
|
const step = { resource: producer.name, node, field: field.key, applied };
|
|
52609
|
-
if (dimension
|
|
52729
|
+
if (dimension) return { path: [step] };
|
|
52610
52730
|
if (field.key === "*") return { path: [{ ...step, applied: ["every column of this producer"] }] };
|
|
52611
52731
|
if (Algo_default.isGlob(field.key)) return { path: [{ ...step, applied: ["every matching column"] }] };
|
|
52612
52732
|
return { path: [step], unresolved: `"${field.key}" is not a column of producer "${producer.name}"` };
|
|
@@ -52652,11 +52772,8 @@ var init_FieldLineage = __esm({
|
|
|
52652
52772
|
...field.hidden ? ["hidden from the export"] : []
|
|
52653
52773
|
];
|
|
52654
52774
|
};
|
|
52655
|
-
/** Every column a producer exposes,
|
|
52656
|
-
this._producerColumns = (producer) => [
|
|
52657
|
-
...(producer.dimensions ?? []).map((x2) => x2.name),
|
|
52658
|
-
...(producer.measures ?? []).map((x2) => x2.name)
|
|
52659
|
-
];
|
|
52775
|
+
/** Every column a producer exposes, in declared order. */
|
|
52776
|
+
this._producerColumns = (producer) => (producer.dimensions ?? []).map((x2) => x2.name);
|
|
52660
52777
|
/** Whether a resource exposes a column by that name. A `*` consumer exposes whatever it is asked. */
|
|
52661
52778
|
this._exposes = (env2, name, column) => {
|
|
52662
52779
|
const producer = (env2.producers ?? []).find((x2) => x2.name === name);
|
|
@@ -52988,7 +53105,6 @@ var init_ResourceView = __esm({
|
|
|
52988
53105
|
],
|
|
52989
53106
|
tables: [
|
|
52990
53107
|
this._dimensionsTable(producer),
|
|
52991
|
-
...producer.measures?.length ? [this._measuresTable(producer)] : [],
|
|
52992
53108
|
this._usedByTable(env2, producer),
|
|
52993
53109
|
this._keyValueTable("Settings", settings)
|
|
52994
53110
|
],
|
|
@@ -53025,16 +53141,6 @@ var init_ResourceView = __esm({
|
|
|
53025
53141
|
]);
|
|
53026
53142
|
return this._table("Dimensions", columns, rows);
|
|
53027
53143
|
};
|
|
53028
|
-
this._measuresTable = (producer) => {
|
|
53029
|
-
const rows = (producer.measures ?? []).map((measure) => [
|
|
53030
|
-
{ value: measure.name, note: this._note(measure.description) },
|
|
53031
|
-
{ value: measure.sql }
|
|
53032
|
-
]);
|
|
53033
|
-
return this._table("Measures", [
|
|
53034
|
-
{ key: "name", label: "Name" },
|
|
53035
|
-
{ key: "sql", label: "SQL" }
|
|
53036
|
-
], rows);
|
|
53037
|
-
};
|
|
53038
53144
|
/**
|
|
53039
53145
|
* Which consumers read each column, the downstream half of field-level lineage. It is the answer
|
|
53040
53146
|
* to "can this column go", which is a question a wide layout raises constantly and which nothing
|
|
@@ -54994,6 +55100,145 @@ var init_RunOverlay = __esm({
|
|
|
54994
55100
|
}
|
|
54995
55101
|
});
|
|
54996
55102
|
|
|
55103
|
+
// ../../packages/engines/src/parsing/ParquetExporter.ts
|
|
55104
|
+
var fs29, PARQUET_TYPES, TRUE_VALUES2, ParquetExporterClass, ParquetExporter, ParquetExporter_default;
|
|
55105
|
+
var init_ParquetExporter = __esm({
|
|
55106
|
+
"../../packages/engines/src/parsing/ParquetExporter.ts"() {
|
|
55107
|
+
fs29 = __toESM(require("fs"), 1);
|
|
55108
|
+
init_src();
|
|
55109
|
+
init_src3();
|
|
55110
|
+
init_src6();
|
|
55111
|
+
init_src2();
|
|
55112
|
+
PARQUET_TYPES = {
|
|
55113
|
+
string: "STRING",
|
|
55114
|
+
number: "DOUBLE",
|
|
55115
|
+
boolean: "BOOLEAN",
|
|
55116
|
+
datetime: "TIMESTAMP"
|
|
55117
|
+
};
|
|
55118
|
+
TRUE_VALUES2 = /^(1|true|yes|y|t)$/i;
|
|
55119
|
+
ParquetExporterClass = class {
|
|
55120
|
+
constructor() {
|
|
55121
|
+
/**
|
|
55122
|
+
* Writes `request.sourcePath` to `request.targetPath` as parquet, SNAPPY compressed, one typed
|
|
55123
|
+
* column per visible consumer field.
|
|
55124
|
+
*
|
|
55125
|
+
* Peak memory is bounded by one row group rather than by the dataset: the writer pulls the rows
|
|
55126
|
+
* a group at a time from the generator below, and nothing holds the file as a whole.
|
|
55127
|
+
*/
|
|
55128
|
+
this.exportToParquet = async (request) => {
|
|
55129
|
+
Affirm_default(request, "Missing required parameter request");
|
|
55130
|
+
Affirm_default(request.sourcePath, "Missing required parameter request.sourcePath");
|
|
55131
|
+
Affirm_default(request.targetPath, "Missing required parameter request.targetPath");
|
|
55132
|
+
Affirm_default(request.dsSchema, "Missing required parameter request.dsSchema");
|
|
55133
|
+
Affirm_default(request.consumerName, "Missing required parameter request.consumerName");
|
|
55134
|
+
const columns = this._columns(request.dsSchema);
|
|
55135
|
+
Affirm_default.hasItems(columns, `Consumer "${request.consumerName}" has no visible field to export as parquet`);
|
|
55136
|
+
const { parquetWriteRows, fileWriter } = await import("hyparquet-writer");
|
|
55137
|
+
const rowGroupSize = this._rowGroupSize(columns.length);
|
|
55138
|
+
const startedMs = Date.now();
|
|
55139
|
+
await parquetWriteRows({
|
|
55140
|
+
writer: fileWriter(request.targetPath),
|
|
55141
|
+
columns,
|
|
55142
|
+
rows: this._readRecords(request, columns),
|
|
55143
|
+
rowGroupSize
|
|
55144
|
+
});
|
|
55145
|
+
const bytes = fs29.statSync(request.targetPath).size;
|
|
55146
|
+
Logger_default.info(`Wrote parquet "${request.targetPath}" for consumer "${request.consumerName}" (${columns.length} column(s), ${rowGroupSize} rows/group, ${bytes} bytes, ${Date.now() - startedMs}ms)`);
|
|
55147
|
+
};
|
|
55148
|
+
/**
|
|
55149
|
+
* The exported schema, taken from the dataset's own: one column per column ON the line, in line
|
|
55150
|
+
* order. Not from the consumer's fields, which cannot know about a shape a `pivot` changed.
|
|
55151
|
+
*/
|
|
55152
|
+
this._columns = (dsSchema) => {
|
|
55153
|
+
return dsSchema.columns.map((column) => ({
|
|
55154
|
+
name: column.name,
|
|
55155
|
+
type: PARQUET_TYPES[column.type] ?? "STRING",
|
|
55156
|
+
nullable: true
|
|
55157
|
+
}));
|
|
55158
|
+
};
|
|
55159
|
+
/**
|
|
55160
|
+
* Rows per row group, derived from a budget of VALUES rather than a fixed row count: a row group
|
|
55161
|
+
* is buffered whole before it can be compressed, so what bounds the export's memory is cells, and
|
|
55162
|
+
* 100,000 rows means 400k values on a 4-column consumer and 130 million on a 1,300-field one.
|
|
55163
|
+
* Clamped at both ends — see the PARQUET_ constants for why each bound is there.
|
|
55164
|
+
*/
|
|
55165
|
+
this._rowGroupSize = (columnCount) => {
|
|
55166
|
+
Affirm_default(columnCount > 0, "Cannot size a row group for zero columns");
|
|
55167
|
+
const { PARQUET_ROW_GROUP_CELLS, PARQUET_MIN_ROW_GROUP_ROWS, PARQUET_MAX_ROW_GROUP_ROWS } = Constants_default.defaults;
|
|
55168
|
+
const rows = Math.floor(PARQUET_ROW_GROUP_CELLS / columnCount);
|
|
55169
|
+
return Math.min(Math.max(rows, PARQUET_MIN_ROW_GROUP_ROWS), PARQUET_MAX_ROW_GROUP_ROWS);
|
|
55170
|
+
};
|
|
55171
|
+
/** A positional line read onto the column names by position — the file holds the visible columns only. */
|
|
55172
|
+
this._positionalValues = (line, columns) => {
|
|
55173
|
+
const parts = DatasetCodec_default.decodeRow(line);
|
|
55174
|
+
const values = {};
|
|
55175
|
+
for (const [index, column] of columns.entries())
|
|
55176
|
+
values[column.name] = parts[index];
|
|
55177
|
+
return values;
|
|
55178
|
+
};
|
|
55179
|
+
/**
|
|
55180
|
+
* One value as its column holds it. A missing value is a null in the column rather than the type's
|
|
55181
|
+
* zero, so a reader can still tell "no value" from a real `0`, `false` or empty string.
|
|
55182
|
+
*
|
|
55183
|
+
* Missing is null/undefined, plus — for every type but `string` — the empty string, matching
|
|
55184
|
+
* `TypeCaster.castDimension`: that is how a NULL arrives from every source that stages through
|
|
55185
|
+
* text. An empty string IS a value for a string column and is kept.
|
|
55186
|
+
*/
|
|
55187
|
+
this._toParquetValue = (value, column, consumerName) => {
|
|
55188
|
+
if (value === null || value === void 0) return null;
|
|
55189
|
+
if (value === "" && column.type !== "STRING") return null;
|
|
55190
|
+
switch (column.type) {
|
|
55191
|
+
case "BOOLEAN":
|
|
55192
|
+
return typeof value === "boolean" ? value : TRUE_VALUES2.test(String(value).trim());
|
|
55193
|
+
case "DOUBLE":
|
|
55194
|
+
return typeof value === "number" ? value : Number(value);
|
|
55195
|
+
case "TIMESTAMP":
|
|
55196
|
+
return this._toDate(value, column.name, consumerName);
|
|
55197
|
+
default:
|
|
55198
|
+
return typeof value === "string" ? value : String(value);
|
|
55199
|
+
}
|
|
55200
|
+
};
|
|
55201
|
+
/**
|
|
55202
|
+
* A `datetime` column's value as a Date. An unparseable one FAILS the run, naming the field and
|
|
55203
|
+
* the value: a consumer whose transformation reformats a date into "DDMMYYYY" under a `datetime`
|
|
55204
|
+
* dimension is a configuration error, and a timestamp column silently full of 1970 hides it until
|
|
55205
|
+
* someone queries the lake.
|
|
55206
|
+
*/
|
|
55207
|
+
this._toDate = (value, name, consumerName) => {
|
|
55208
|
+
const date2 = value instanceof Date ? value : new Date(value);
|
|
55209
|
+
if (isNaN(date2.getTime()))
|
|
55210
|
+
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.`);
|
|
55211
|
+
return date2;
|
|
55212
|
+
};
|
|
55213
|
+
}
|
|
55214
|
+
/**
|
|
55215
|
+
* The dataset's records, one at a time, coerced to what each column holds. An async generator, so
|
|
55216
|
+
* the writer pulls a row group's worth and no more — the file is never in memory as a whole.
|
|
55217
|
+
*
|
|
55218
|
+
* A parquet consumer stages JSON (see `DatasetSchema.encodingFor`), so the values arrive already
|
|
55219
|
+
* typed. The positional branch covers a dataset staged the other way, reading the row WITHOUT
|
|
55220
|
+
* casting since the cast is what loses a boolean (`Boolean('false')` is `true`).
|
|
55221
|
+
*/
|
|
55222
|
+
async *_readRecords(request, columns) {
|
|
55223
|
+
const readStream = fs29.createReadStream(request.sourcePath);
|
|
55224
|
+
try {
|
|
55225
|
+
for await (const line of readLines(readStream)) {
|
|
55226
|
+
const values = request.dsSchema.encoding === "CSV" ? this._positionalValues(line, columns) : JSON.parse(line);
|
|
55227
|
+
const row = {};
|
|
55228
|
+
for (const column of columns)
|
|
55229
|
+
row[column.name] = this._toParquetValue(values[column.name], column, request.consumerName);
|
|
55230
|
+
yield row;
|
|
55231
|
+
}
|
|
55232
|
+
} finally {
|
|
55233
|
+
readStream.destroy();
|
|
55234
|
+
}
|
|
55235
|
+
}
|
|
55236
|
+
};
|
|
55237
|
+
ParquetExporter = new ParquetExporterClass();
|
|
55238
|
+
ParquetExporter_default = ParquetExporter;
|
|
55239
|
+
}
|
|
55240
|
+
});
|
|
55241
|
+
|
|
54997
55242
|
// ../../packages/engines/src/producer/PushdownEngine.ts
|
|
54998
55243
|
var MISSING_BY_TYPE, PushdownEngineClass, PushdownEngine, PushdownEngine_default;
|
|
54999
55244
|
var init_PushdownEngine = __esm({
|
|
@@ -55826,6 +56071,7 @@ var init_src10 = __esm({
|
|
|
55826
56071
|
init_RunOverlay();
|
|
55827
56072
|
init_RunRegistry();
|
|
55828
56073
|
init_LineParser();
|
|
56074
|
+
init_ParquetExporter();
|
|
55829
56075
|
init_DiscoverMerger();
|
|
55830
56076
|
init_ProducerEngine();
|
|
55831
56077
|
init_ProducerManager();
|
|
@@ -55841,6 +56087,108 @@ var init_src10 = __esm({
|
|
|
55841
56087
|
}
|
|
55842
56088
|
});
|
|
55843
56089
|
|
|
56090
|
+
// ../../packages/executors/src/DatasetSchema.ts
|
|
56091
|
+
var DatasetSchemaClass, DatasetSchema, DatasetSchema_default;
|
|
56092
|
+
var init_DatasetSchema = __esm({
|
|
56093
|
+
"../../packages/executors/src/DatasetSchema.ts"() {
|
|
56094
|
+
init_src();
|
|
56095
|
+
init_src10();
|
|
56096
|
+
DatasetSchemaClass = class {
|
|
56097
|
+
constructor() {
|
|
56098
|
+
/**
|
|
56099
|
+
* How a consumer's records are staged on the way to the destination. A consumer declares exactly
|
|
56100
|
+
* one output (`maxItems: 1` in consumer-schema.json), so `outputs[0]` is THE output.
|
|
56101
|
+
*/
|
|
56102
|
+
this.encodingFor = (consumer) => {
|
|
56103
|
+
Affirm_default(consumer, "Invalid consumer");
|
|
56104
|
+
Affirm_default.hasItems(consumer.outputs, `Consumer "${consumer.name}" declares no output`);
|
|
56105
|
+
switch (consumer.outputs[0].format) {
|
|
56106
|
+
case "JSON":
|
|
56107
|
+
case "API":
|
|
56108
|
+
return "JSON";
|
|
56109
|
+
case "PARQUET":
|
|
56110
|
+
return "JSON";
|
|
56111
|
+
case "TXT":
|
|
56112
|
+
return "CSV";
|
|
56113
|
+
case "CSV":
|
|
56114
|
+
return "CSV";
|
|
56115
|
+
default:
|
|
56116
|
+
throw new Error(`Export format ${consumer.outputs[0].format} not implemented yet.`);
|
|
56117
|
+
}
|
|
56118
|
+
};
|
|
56119
|
+
/**
|
|
56120
|
+
* The schema the run's workers write with: one column per VISIBLE consumer field, in field
|
|
56121
|
+
* order. Hidden fields are not on the line at all.
|
|
56122
|
+
*/
|
|
56123
|
+
this.compile = (consumer, fields) => {
|
|
56124
|
+
Affirm_default(consumer, "Invalid consumer");
|
|
56125
|
+
Affirm_default(fields, "Invalid consumer fields");
|
|
56126
|
+
const columns = fields.filter((x2) => !x2.cField.hidden).map(this._column);
|
|
56127
|
+
Affirm_default.hasItems(columns, `Consumer "${consumer.name}" has no visible field to write`);
|
|
56128
|
+
return {
|
|
56129
|
+
encoding: this.encodingFor(consumer),
|
|
56130
|
+
columns,
|
|
56131
|
+
rows: 0,
|
|
56132
|
+
writtenBy: "worker"
|
|
56133
|
+
};
|
|
56134
|
+
};
|
|
56135
|
+
/**
|
|
56136
|
+
* The same shape, as the named stage left it. Returned by every pass that rewrites the dataset
|
|
56137
|
+
* without changing its columns, so `rows` always describes the file as it is NOW.
|
|
56138
|
+
*/
|
|
56139
|
+
this.rewritten = (dsSchema, writtenBy, rows) => {
|
|
56140
|
+
Affirm_default(dsSchema, "Invalid dataset schema");
|
|
56141
|
+
Affirm_default(rows >= 0, `Invalid row count ${rows} for a dataset written by "${writtenBy}"`);
|
|
56142
|
+
return { ...dsSchema, writtenBy, rows };
|
|
56143
|
+
};
|
|
56144
|
+
/**
|
|
56145
|
+
* The shape a `pivot` leaves behind: the row keys it grouped on, then one column per pivot value.
|
|
56146
|
+
* A pivoted column has no producer dimension behind it - it is an aggregate this pass invented -
|
|
56147
|
+
* so it declares `source: 'pivot'` and a `number` type.
|
|
56148
|
+
*/
|
|
56149
|
+
this.forPivot = (dsSchema, rowKeys, pivotColumns, rows) => {
|
|
56150
|
+
Affirm_default(dsSchema, "Invalid dataset schema");
|
|
56151
|
+
Affirm_default.hasItems(rowKeys, "A pivot declares no row key");
|
|
56152
|
+
const keyColumns = rowKeys.map((key) => {
|
|
56153
|
+
const existing = dsSchema.columns.find((column) => column.name === key);
|
|
56154
|
+
Affirm_default(existing, `The pivot row key "${key}" is not a column of the dataset being pivoted`);
|
|
56155
|
+
return existing;
|
|
56156
|
+
});
|
|
56157
|
+
return {
|
|
56158
|
+
encoding: dsSchema.encoding,
|
|
56159
|
+
columns: [...keyColumns, ...pivotColumns.map((name) => ({ name, type: "number", source: "pivot" }))],
|
|
56160
|
+
rows,
|
|
56161
|
+
writtenBy: "pivot"
|
|
56162
|
+
};
|
|
56163
|
+
};
|
|
56164
|
+
/** The column names on a line, in line order - the header of a positional export. */
|
|
56165
|
+
this.names = (dsSchema) => {
|
|
56166
|
+
Affirm_default(dsSchema, "Invalid dataset schema");
|
|
56167
|
+
return dsSchema.columns.map((column) => column.name);
|
|
56168
|
+
};
|
|
56169
|
+
/**
|
|
56170
|
+
* One column, typed from the dimension behind it. `type`, `format` and `nullable` are the only
|
|
56171
|
+
* three properties a cast reads, so carrying those lets a reader cast a staged value back
|
|
56172
|
+
* without going near the consumer config.
|
|
56173
|
+
*/
|
|
56174
|
+
this._column = (field) => {
|
|
56175
|
+
if (ConsumerManager_default.isLineCountField(field.cField))
|
|
56176
|
+
return { name: field.finalKey, type: "number", source: "lineCount" };
|
|
56177
|
+
const column = {
|
|
56178
|
+
name: field.finalKey,
|
|
56179
|
+
type: field.dimension?.type ?? "string"
|
|
56180
|
+
};
|
|
56181
|
+
if (field.dimension?.format) column.format = field.dimension.format;
|
|
56182
|
+
if (field.dimension?.nullable === false) column.nullable = false;
|
|
56183
|
+
return column;
|
|
56184
|
+
};
|
|
56185
|
+
}
|
|
56186
|
+
};
|
|
56187
|
+
DatasetSchema = new DatasetSchemaClass();
|
|
56188
|
+
DatasetSchema_default = DatasetSchema;
|
|
56189
|
+
}
|
|
56190
|
+
});
|
|
56191
|
+
|
|
55844
56192
|
// ../../packages/executors/src/OutputExecutor.ts
|
|
55845
56193
|
var OutputExecutorClass, OutputExecutor, OutputExecutor_default;
|
|
55846
56194
|
var init_OutputExecutor = __esm({
|
|
@@ -55849,35 +56197,24 @@ var init_OutputExecutor = __esm({
|
|
|
55849
56197
|
init_src();
|
|
55850
56198
|
init_src8();
|
|
55851
56199
|
init_src4();
|
|
56200
|
+
init_src10();
|
|
55852
56201
|
init_src6();
|
|
55853
56202
|
init_src2();
|
|
56203
|
+
init_DatasetSchema();
|
|
55854
56204
|
OutputExecutorClass = class {
|
|
55855
56205
|
constructor() {
|
|
55856
|
-
this._getInternalRecordFormat = (consumer) => {
|
|
55857
|
-
const output = consumer.outputs[0];
|
|
55858
|
-
switch (output.format) {
|
|
55859
|
-
case "API":
|
|
55860
|
-
return "JSON";
|
|
55861
|
-
case "TXT":
|
|
55862
|
-
return "CSV";
|
|
55863
|
-
default:
|
|
55864
|
-
return output.format;
|
|
55865
|
-
}
|
|
55866
|
-
};
|
|
55867
56206
|
/**
|
|
55868
|
-
* The
|
|
55869
|
-
*
|
|
55870
|
-
* all three for every record, and `toCSV` allocated a filtered field array on top of that.
|
|
56207
|
+
* The write side of a staged dataset's schema: what to write, in what order, in which encoding.
|
|
56208
|
+
* Derived from the schema, so the writer and every reader share one column list.
|
|
55871
56209
|
*/
|
|
55872
|
-
this.compileOutput = (consumer,
|
|
56210
|
+
this.compileOutput = (consumer, dsSchema) => {
|
|
55873
56211
|
Affirm_default(consumer, "Invalid consumer");
|
|
55874
|
-
Affirm_default(
|
|
56212
|
+
Affirm_default(dsSchema, "Invalid dataset schema");
|
|
55875
56213
|
return {
|
|
55876
|
-
format:
|
|
56214
|
+
format: dsSchema.encoding,
|
|
55877
56215
|
delimiter: ",",
|
|
55878
|
-
visibleKeys:
|
|
55879
|
-
|
|
55880
|
-
omitNull: consumer.outputs.some((x2) => x2.omitNull === true)
|
|
56216
|
+
visibleKeys: DatasetSchema_default.names(dsSchema),
|
|
56217
|
+
omitNull: consumer.outputs[0].omitNull === true
|
|
55881
56218
|
};
|
|
55882
56219
|
};
|
|
55883
56220
|
this.outputRecord = (record, plan) => {
|
|
@@ -55890,26 +56227,38 @@ var init_OutputExecutor = __esm({
|
|
|
55890
56227
|
throw new Error(`Export format ${plan.format} not implemented yet.`);
|
|
55891
56228
|
}
|
|
55892
56229
|
};
|
|
56230
|
+
/**
|
|
56231
|
+
* One positional line of the internal format. `DatasetCodec`, not `CSVParser`: a staged line has
|
|
56232
|
+
* to carry a null that is not an empty string and must never hold a newline.
|
|
56233
|
+
*/
|
|
55893
56234
|
this.toCSV = (record, plan) => {
|
|
55894
56235
|
const { visibleKeys, delimiter } = plan;
|
|
55895
56236
|
let row = "";
|
|
55896
56237
|
for (let index = 0; index < visibleKeys.length; index++) {
|
|
55897
56238
|
if (index > 0)
|
|
55898
56239
|
row += delimiter;
|
|
55899
|
-
row +=
|
|
56240
|
+
row += DatasetCodec_default.encodeField(record[visibleKeys[index]]);
|
|
55900
56241
|
}
|
|
55901
56242
|
return row;
|
|
55902
56243
|
};
|
|
56244
|
+
/**
|
|
56245
|
+
* One JSON object per line, keyed in the CONSUMER's field order rather than the record's own.
|
|
56246
|
+
*
|
|
56247
|
+
* A record arrives keyed in the order its producer's dimensions were read, so two producers of
|
|
56248
|
+
* one consumer wrote the same record as two different lines — and `distinct` and
|
|
56249
|
+
* `no_duplicates`, which compare the line, saw two rows. Building from `visibleKeys` gives every
|
|
56250
|
+
* writer one order, and drops the hidden fields without deleting them out of the caller's record.
|
|
56251
|
+
*/
|
|
55903
56252
|
this.toJSON = (record, plan) => {
|
|
55904
|
-
|
|
55905
|
-
|
|
55906
|
-
|
|
55907
|
-
|
|
55908
|
-
|
|
55909
|
-
|
|
55910
|
-
|
|
56253
|
+
const { visibleKeys, omitNull } = plan;
|
|
56254
|
+
const output = {};
|
|
56255
|
+
for (const key of visibleKeys) {
|
|
56256
|
+
const value = record[key];
|
|
56257
|
+
if (omitNull && (value === null || value === void 0))
|
|
56258
|
+
continue;
|
|
56259
|
+
output[key] = value === void 0 ? null : value;
|
|
55911
56260
|
}
|
|
55912
|
-
return JSON.stringify(
|
|
56261
|
+
return JSON.stringify(output);
|
|
55913
56262
|
};
|
|
55914
56263
|
/**
|
|
55915
56264
|
* The exported name of one split file: the destination name with the file's label before the
|
|
@@ -55922,66 +56271,80 @@ var init_OutputExecutor = __esm({
|
|
|
55922
56271
|
return `${pathName.substring(0, dot)}_${label}${pathName.substring(dot)}`;
|
|
55923
56272
|
};
|
|
55924
56273
|
/**
|
|
55925
|
-
* Writes the run's result to
|
|
56274
|
+
* Writes the run's result to the consumer's destination, one call per file the split pass
|
|
55926
56275
|
* produced. The files are passed in rather than read back out of the scope folder: the label a
|
|
55927
56276
|
* file carries is what names it at the destination, and with no split configured that list is
|
|
55928
56277
|
* the merged dataset alone, labelled "0" — the suffix an unsplit export has always had.
|
|
56278
|
+
*
|
|
56279
|
+
* One output per consumer (`maxItems: 1` in consumer-schema.json). This used to loop, but only
|
|
56280
|
+
* ever shipped the first entry, and could not have done more: `move` unlinks what it ships.
|
|
55929
56281
|
*/
|
|
55930
|
-
this.exportResult = async (consumer,
|
|
56282
|
+
this.exportResult = async (consumer, dsSchema, splitFiles) => {
|
|
55931
56283
|
Affirm_default.hasItems(splitFiles, `Consumer "${consumer.name}" has no output file to export`);
|
|
55932
|
-
|
|
56284
|
+
Affirm_default.hasItems(consumer.outputs, `Consumer "${consumer.name}" declares no output to export to`);
|
|
56285
|
+
Affirm_default(dsSchema, `Consumer "${consumer.name}" has no dataset schema to export against`);
|
|
56286
|
+
const output = consumer.outputs[0];
|
|
56287
|
+
const internalFormat = dsSchema.encoding;
|
|
56288
|
+
const destination = Environment_default.getSource(output.exportDestination);
|
|
56289
|
+
Affirm_default(destination, `The export destination "${output.exportDestination}" of consumer "${consumer.name}" is not a source of this project`);
|
|
56290
|
+
const driver = await DriverFactory_default.instantiateDestination(destination);
|
|
56291
|
+
const destinationName = this._composeFileName(consumer, output, this._getExtension(output));
|
|
56292
|
+
Logger_default.info(`Exporting consumer "${consumer.name}" to "${output.exportDestination}" as ${output.format} (${destinationName}, ${splitFiles.length} file(s))`);
|
|
56293
|
+
const header = this._composeHeader(output, dsSchema);
|
|
55933
56294
|
const results = [];
|
|
55934
|
-
for (const
|
|
55935
|
-
const
|
|
55936
|
-
|
|
55937
|
-
|
|
55938
|
-
|
|
55939
|
-
|
|
55940
|
-
|
|
55941
|
-
|
|
55942
|
-
|
|
55943
|
-
results.push(
|
|
55944
|
-
|
|
55945
|
-
|
|
55946
|
-
|
|
55947
|
-
|
|
55948
|
-
|
|
55949
|
-
|
|
55950
|
-
|
|
55951
|
-
|
|
55952
|
-
);
|
|
55953
|
-
break;
|
|
55954
|
-
case "TXT": {
|
|
55955
|
-
const delimiter = this._getDelimiter(output);
|
|
55956
|
-
results.push(
|
|
55957
|
-
await driver.transformAndMove(
|
|
55958
|
-
startingPath,
|
|
55959
|
-
(line) => CSVParser_default.stringifyRow(CSVParser_default.parseRow(line, ","), delimiter),
|
|
55960
|
-
destinationPath,
|
|
55961
|
-
header
|
|
55962
|
-
)
|
|
55963
|
-
);
|
|
55964
|
-
break;
|
|
55965
|
-
}
|
|
55966
|
-
case "API":
|
|
55967
|
-
case "JSON":
|
|
55968
|
-
results.push(
|
|
55969
|
-
await driver.transformAndMove(startingPath, (line) => {
|
|
55970
|
-
const parts = CSVParser_default.parseRow(line, ",");
|
|
55971
|
-
const value = {};
|
|
55972
|
-
for (const [index, field] of fields.entries())
|
|
55973
|
-
value[field.finalKey] = parts[index];
|
|
55974
|
-
return JSON.stringify(value);
|
|
55975
|
-
}, destinationPath)
|
|
55976
|
-
);
|
|
55977
|
-
break;
|
|
55978
|
-
default:
|
|
55979
|
-
throw new Error(`Export result to format ${output.format} not implemented yet.`);
|
|
55980
|
-
}
|
|
56295
|
+
for (const { path: startingPath, label } of splitFiles) {
|
|
56296
|
+
const destinationPath = this.getCompletedPath(destinationName, label);
|
|
56297
|
+
if (output.format === internalFormat || output.format === "API") {
|
|
56298
|
+
results.push(await driver.move(startingPath, destinationPath, header));
|
|
56299
|
+
continue;
|
|
56300
|
+
}
|
|
56301
|
+
switch (output.format) {
|
|
56302
|
+
case "TXT": {
|
|
56303
|
+
const delimiter = this._getDelimiter(output);
|
|
56304
|
+
results.push(
|
|
56305
|
+
await driver.transformAndMove(
|
|
56306
|
+
startingPath,
|
|
56307
|
+
(line) => DatasetCodec_default.redelimit(line, delimiter),
|
|
56308
|
+
destinationPath,
|
|
56309
|
+
header
|
|
56310
|
+
)
|
|
56311
|
+
);
|
|
56312
|
+
break;
|
|
55981
56313
|
}
|
|
56314
|
+
case "PARQUET":
|
|
56315
|
+
results.push(await this._exportParquet({
|
|
56316
|
+
sourcePath: startingPath,
|
|
56317
|
+
destinationPath,
|
|
56318
|
+
dsSchema,
|
|
56319
|
+
consumerName: consumer.name,
|
|
56320
|
+
driver
|
|
56321
|
+
}));
|
|
56322
|
+
break;
|
|
56323
|
+
default:
|
|
56324
|
+
throw new Error(`Export result to format ${output.format} not implemented yet.`);
|
|
55982
56325
|
}
|
|
55983
|
-
return this._unifingResult(results);
|
|
55984
56326
|
}
|
|
56327
|
+
return this._unifingResult(results);
|
|
56328
|
+
};
|
|
56329
|
+
/**
|
|
56330
|
+
* Ships one split file as parquet. A parquet file is a columnar container whose footer can only
|
|
56331
|
+
* be written once the last row group is encoded, so there is no per-line mapping to hand a
|
|
56332
|
+
* driver: the file is built in the scope folder beside the dataset it is read from, then handed
|
|
56333
|
+
* over as bytes with NO header — a parquet file carries its own schema, and `move` is
|
|
56334
|
+
* byte-faithful in all three destination drivers.
|
|
56335
|
+
*
|
|
56336
|
+
* `move` unlinks what it shipped, so a successful export leaves no staged file behind. A failed
|
|
56337
|
+
* one leaves it in the scope folder, which the run's cleanup removes with everything else.
|
|
56338
|
+
*/
|
|
56339
|
+
this._exportParquet = async (plan) => {
|
|
56340
|
+
const parquetPath = `${plan.sourcePath}.parquet`;
|
|
56341
|
+
await ParquetExporter_default.exportToParquet({
|
|
56342
|
+
sourcePath: plan.sourcePath,
|
|
56343
|
+
targetPath: parquetPath,
|
|
56344
|
+
dsSchema: plan.dsSchema,
|
|
56345
|
+
consumerName: plan.consumerName
|
|
56346
|
+
});
|
|
56347
|
+
return plan.driver.move(parquetPath, plan.destinationPath);
|
|
55985
56348
|
};
|
|
55986
56349
|
/**
|
|
55987
56350
|
* Folds the per-file upload results into the one the run reports.
|
|
@@ -56003,21 +56366,28 @@ var init_OutputExecutor = __esm({
|
|
|
56003
56366
|
* does. Null for a JSON/API output, where a record already carries its own field names, and for
|
|
56004
56367
|
* an output that turns the header off.
|
|
56005
56368
|
*
|
|
56006
|
-
* The names
|
|
56007
|
-
* of the file has nothing else to match a column against.
|
|
56369
|
+
* The names come from the dataset's own schema, so a `pivot` is described by the header.
|
|
56008
56370
|
*/
|
|
56009
|
-
this._composeHeader = (output,
|
|
56371
|
+
this._composeHeader = (output, dsSchema) => {
|
|
56010
56372
|
if (output.header === false) return null;
|
|
56011
56373
|
if (output.format !== "CSV" && output.format !== "TXT") return null;
|
|
56012
|
-
|
|
56013
|
-
return CSVParser_default.stringifyRow(names, this._getDelimiter(output));
|
|
56374
|
+
return CSVParser_default.stringifyRow(DatasetSchema_default.names(dsSchema), this._getDelimiter(output));
|
|
56014
56375
|
};
|
|
56015
56376
|
/** The delimiter the exported file is written with. Only TXT configures it; CSV is a comma. */
|
|
56016
56377
|
this._getDelimiter = (output) => {
|
|
56017
56378
|
return output.format === "TXT" ? output.delimiter ?? " " : ",";
|
|
56018
56379
|
};
|
|
56019
56380
|
this._getExtension = (output) => {
|
|
56020
|
-
|
|
56381
|
+
switch (output.format) {
|
|
56382
|
+
case "CSV":
|
|
56383
|
+
return "csv";
|
|
56384
|
+
case "JSON":
|
|
56385
|
+
return "jsonl";
|
|
56386
|
+
case "PARQUET":
|
|
56387
|
+
return "parquet";
|
|
56388
|
+
default:
|
|
56389
|
+
return "txt";
|
|
56390
|
+
}
|
|
56021
56391
|
};
|
|
56022
56392
|
this._composeFileName = (consumer, output, extension, executionId) => {
|
|
56023
56393
|
if (output.exportName && output.exportName.trim().length > 0) {
|
|
@@ -56049,7 +56419,7 @@ var init_ConsumerExecutor = __esm({
|
|
|
56049
56419
|
init_src();
|
|
56050
56420
|
init_src10();
|
|
56051
56421
|
init_OutputExecutor();
|
|
56052
|
-
|
|
56422
|
+
init_DatasetSchema();
|
|
56053
56423
|
init_src2();
|
|
56054
56424
|
LIMIT_SCAN_BYTES = 64 * 1024;
|
|
56055
56425
|
NEWLINE_BYTE = 10;
|
|
@@ -56173,13 +56543,20 @@ var init_ConsumerExecutor = __esm({
|
|
|
56173
56543
|
}
|
|
56174
56544
|
return output;
|
|
56175
56545
|
};
|
|
56176
|
-
|
|
56177
|
-
|
|
56178
|
-
|
|
56179
|
-
|
|
56180
|
-
|
|
56181
|
-
|
|
56182
|
-
|
|
56546
|
+
/**
|
|
56547
|
+
* Rewrites every `lineCount` field of the merged dataset so it names the line of the SOURCE file
|
|
56548
|
+
* the row came from, by adding each chunk's `rowOffset` to the counter its worker wrote.
|
|
56549
|
+
*
|
|
56550
|
+
* The shape is unchanged, so the schema comes back with a fresh `writtenBy` and the row count
|
|
56551
|
+
* this pass actually saw - which also checks the file still holds what the workers reported.
|
|
56552
|
+
*/
|
|
56553
|
+
this.processLineCount = async (consumer, dsSchema, datasetPath, executorResults) => {
|
|
56554
|
+
Affirm_default(dsSchema, "Invalid dataset schema");
|
|
56555
|
+
const lineCountColumns = dsSchema.columns.filter((column) => column.source === "lineCount");
|
|
56556
|
+
if (lineCountColumns.length === 0)
|
|
56557
|
+
return dsSchema;
|
|
56558
|
+
const parseLine = LineParser_default.datasetParser(dsSchema);
|
|
56559
|
+
const outputPlan = OutputExecutor_default.compileOutput(consumer, dsSchema);
|
|
56183
56560
|
const tempWorkPath = datasetPath + "_tmp";
|
|
56184
56561
|
const reader = import_fs24.default.createReadStream(datasetPath);
|
|
56185
56562
|
const writer = import_fs24.default.createWriteStream(tempWorkPath);
|
|
@@ -56201,11 +56578,11 @@ var init_ConsumerExecutor = __esm({
|
|
|
56201
56578
|
Affirm_default(currentWorker, `Unable to resolve worker metadata while processing lineCount for ${datasetPath}`);
|
|
56202
56579
|
const rowOffset = currentWorker.rowOffset ?? 0;
|
|
56203
56580
|
if (rowOffset > 0) {
|
|
56204
|
-
const record =
|
|
56205
|
-
for (const
|
|
56206
|
-
const currentValue = Number(record[
|
|
56581
|
+
const record = parseLine(line);
|
|
56582
|
+
for (const column of lineCountColumns) {
|
|
56583
|
+
const currentValue = Number(record[column.name]);
|
|
56207
56584
|
Affirm_default(!Number.isNaN(currentValue), `Invalid lineCount value found while processing ${datasetPath}`);
|
|
56208
|
-
record[
|
|
56585
|
+
record[column.name] = currentValue + rowOffset;
|
|
56209
56586
|
}
|
|
56210
56587
|
if (!writer.write(OutputExecutor_default.outputRecord(record, outputPlan) + "\n"))
|
|
56211
56588
|
await waitForDrain();
|
|
@@ -56228,11 +56605,11 @@ var init_ConsumerExecutor = __esm({
|
|
|
56228
56605
|
}
|
|
56229
56606
|
await import_promises16.default.unlink(datasetPath);
|
|
56230
56607
|
await import_promises16.default.rename(tempWorkPath, datasetPath);
|
|
56231
|
-
return processedLineCount;
|
|
56608
|
+
return DatasetSchema_default.rewritten(dsSchema, "lineCount", processedLineCount);
|
|
56232
56609
|
};
|
|
56233
56610
|
/**
|
|
56234
|
-
* Cuts the merged dataset down to the run's record limit, and reports the
|
|
56235
|
-
*
|
|
56611
|
+
* Cuts the merged dataset down to the run's record limit, and reports the shape it left behind.
|
|
56612
|
+
* The schema comes back unchanged when the dataset was already within the limit.
|
|
56236
56613
|
*
|
|
56237
56614
|
* Every worker stops at the limit within its OWN chunk, which is the right thing for the read
|
|
56238
56615
|
* (no thread reads further than the run could need) but leaves up to `limit` rows per chunk in
|
|
@@ -56242,7 +56619,8 @@ var init_ConsumerExecutor = __esm({
|
|
|
56242
56619
|
* at then needs no arithmetic on line contents, so no line ending or multi-byte character can
|
|
56243
56620
|
* put the cut in the middle of a row. Only the bytes up to the limit are ever read.
|
|
56244
56621
|
*/
|
|
56245
|
-
this.processLimit = async (datasetPath, limit) => {
|
|
56622
|
+
this.processLimit = async (dsSchema, datasetPath, limit) => {
|
|
56623
|
+
Affirm_default(dsSchema, "Invalid dataset schema");
|
|
56246
56624
|
Affirm_default(datasetPath, "Invalid dataset path");
|
|
56247
56625
|
Affirm_default(limit > 0, `Invalid record limit ${limit}`);
|
|
56248
56626
|
const handle = await import_promises16.default.open(datasetPath, "r+");
|
|
@@ -56253,7 +56631,7 @@ var init_ConsumerExecutor = __esm({
|
|
|
56253
56631
|
while (newlines < limit) {
|
|
56254
56632
|
const { bytesRead } = await handle.read(buffer, 0, LIMIT_SCAN_BYTES, offset);
|
|
56255
56633
|
if (bytesRead === 0)
|
|
56256
|
-
return
|
|
56634
|
+
return dsSchema;
|
|
56257
56635
|
for (let index = 0; index < bytesRead; index++) {
|
|
56258
56636
|
offset++;
|
|
56259
56637
|
if (buffer[index] !== NEWLINE_BYTE) continue;
|
|
@@ -56263,14 +56641,15 @@ var init_ConsumerExecutor = __esm({
|
|
|
56263
56641
|
}
|
|
56264
56642
|
const { size } = await handle.stat();
|
|
56265
56643
|
if (offset >= size)
|
|
56266
|
-
return
|
|
56644
|
+
return dsSchema;
|
|
56267
56645
|
await handle.truncate(offset);
|
|
56268
|
-
return limit;
|
|
56646
|
+
return DatasetSchema_default.rewritten(dsSchema, "limit", limit);
|
|
56269
56647
|
} finally {
|
|
56270
56648
|
await handle.close();
|
|
56271
56649
|
}
|
|
56272
56650
|
};
|
|
56273
|
-
this.processDistinct = async (datasetPath) => {
|
|
56651
|
+
this.processDistinct = async (dsSchema, datasetPath) => {
|
|
56652
|
+
Affirm_default(dsSchema, "Invalid dataset schema");
|
|
56274
56653
|
const reader = import_fs24.default.createReadStream(datasetPath);
|
|
56275
56654
|
const tempWorkPath = datasetPath + "_tmp";
|
|
56276
56655
|
const writer = import_fs24.default.createWriteStream(tempWorkPath);
|
|
@@ -56299,18 +56678,18 @@ var init_ConsumerExecutor = __esm({
|
|
|
56299
56678
|
}
|
|
56300
56679
|
await import_promises16.default.unlink(datasetPath);
|
|
56301
56680
|
await import_promises16.default.rename(tempWorkPath, datasetPath);
|
|
56302
|
-
return newLineCount;
|
|
56681
|
+
return DatasetSchema_default.rewritten(dsSchema, "distinct", newLineCount);
|
|
56303
56682
|
};
|
|
56304
|
-
this.processDistinctOn = async (consumer, datasetPath) => {
|
|
56683
|
+
this.processDistinctOn = async (consumer, dsSchema, datasetPath) => {
|
|
56684
|
+
Affirm_default(dsSchema, "Invalid dataset schema");
|
|
56305
56685
|
const reader = import_fs24.default.createReadStream(datasetPath);
|
|
56306
56686
|
const { distinctOn } = consumer.options;
|
|
56307
56687
|
const { keys, resolution } = distinctOn;
|
|
56308
56688
|
const { strategy, orderBy, direction = "asc" } = resolution;
|
|
56309
|
-
const
|
|
56310
|
-
const internalFields = ConsumerManager_default.getExpandedFields(consumer);
|
|
56689
|
+
const parseLine = LineParser_default.datasetParser(dsSchema);
|
|
56311
56690
|
const winners = /* @__PURE__ */ new Map();
|
|
56312
56691
|
for await (const line of readLines(reader)) {
|
|
56313
|
-
const record =
|
|
56692
|
+
const record = parseLine(line);
|
|
56314
56693
|
const compositeKey = keys.map((k7) => String(record[k7] ?? "")).join("|");
|
|
56315
56694
|
const existing = winners.get(compositeKey);
|
|
56316
56695
|
if (!existing) {
|
|
@@ -56342,20 +56721,27 @@ var init_ConsumerExecutor = __esm({
|
|
|
56342
56721
|
}
|
|
56343
56722
|
await import_promises16.default.unlink(datasetPath);
|
|
56344
56723
|
await import_promises16.default.rename(tempWorkPath, datasetPath);
|
|
56345
|
-
return winners.size;
|
|
56724
|
+
return DatasetSchema_default.rewritten(dsSchema, "distinctOn", winners.size);
|
|
56346
56725
|
};
|
|
56347
|
-
|
|
56726
|
+
/**
|
|
56727
|
+
* Groups the dataset by `rowKeys` and turns one column's values into columns of their own.
|
|
56728
|
+
*
|
|
56729
|
+
* This is the one pass that changes the SHAPE of the file, which is why it returns the schema it
|
|
56730
|
+
* wrote rather than a row count: the header, the split pass, the dataset validation and the
|
|
56731
|
+
* parquet schema all read the result.
|
|
56732
|
+
*/
|
|
56733
|
+
this.processPivot = async (consumer, dsSchema, datasetPath) => {
|
|
56734
|
+
Affirm_default(dsSchema, "Invalid dataset schema");
|
|
56348
56735
|
const { pivot } = consumer.options;
|
|
56349
56736
|
const { rowKeys, pivotColumn, valueColumn, aggregation, columnPrefix = "" } = pivot;
|
|
56350
|
-
const
|
|
56351
|
-
const internalFields = ConsumerManager_default.getExpandedFields(consumer);
|
|
56737
|
+
const parseLine = LineParser_default.datasetParser(dsSchema);
|
|
56352
56738
|
let pivotValues = pivot.pivotValues;
|
|
56353
56739
|
if (!pivotValues) {
|
|
56354
56740
|
pivotValues = [];
|
|
56355
56741
|
const discoverySet = /* @__PURE__ */ new Set();
|
|
56356
56742
|
const discoverReader = import_fs24.default.createReadStream(datasetPath);
|
|
56357
56743
|
for await (const line of readLines(discoverReader)) {
|
|
56358
|
-
const record =
|
|
56744
|
+
const record = parseLine(line);
|
|
56359
56745
|
const val = String(record[pivotColumn] ?? "");
|
|
56360
56746
|
if (!discoverySet.has(val)) {
|
|
56361
56747
|
discoverySet.add(val);
|
|
@@ -56372,7 +56758,7 @@ var init_ConsumerExecutor = __esm({
|
|
|
56372
56758
|
const groups = /* @__PURE__ */ new Map();
|
|
56373
56759
|
const reader = import_fs24.default.createReadStream(datasetPath);
|
|
56374
56760
|
for await (const line of readLines(reader)) {
|
|
56375
|
-
const record =
|
|
56761
|
+
const record = parseLine(line);
|
|
56376
56762
|
const compositeKey = rowKeys.map((k7) => String(record[k7] ?? "")).join("|");
|
|
56377
56763
|
const pivotVal = String(record[pivotColumn] ?? "");
|
|
56378
56764
|
const numericVal = Number(record[valueColumn]) || 0;
|
|
@@ -56391,11 +56777,8 @@ var init_ConsumerExecutor = __esm({
|
|
|
56391
56777
|
cell.min = Math.min(cell.min, numericVal);
|
|
56392
56778
|
cell.max = Math.max(cell.max, numericVal);
|
|
56393
56779
|
}
|
|
56394
|
-
const
|
|
56395
|
-
|
|
56396
|
-
...pivotValues.map((pv) => ({ cField: { key: columnPrefix + pv }, finalKey: columnPrefix + pv }))
|
|
56397
|
-
];
|
|
56398
|
-
const outputPlan = OutputExecutor_default.compileOutput(consumer, pivotedFields);
|
|
56780
|
+
const pivotedDsSchema = DatasetSchema_default.forPivot(dsSchema, rowKeys, pivotValues.map((pv) => columnPrefix + pv), groups.size);
|
|
56781
|
+
const outputPlan = OutputExecutor_default.compileOutput(consumer, pivotedDsSchema);
|
|
56399
56782
|
const tempWorkPath = datasetPath + "_tmp";
|
|
56400
56783
|
const writer = import_fs24.default.createWriteStream(tempWorkPath);
|
|
56401
56784
|
const waitForDrain = () => new Promise((resolve) => writer.once("drain", resolve));
|
|
@@ -56445,10 +56828,8 @@ var init_ConsumerExecutor = __esm({
|
|
|
56445
56828
|
}
|
|
56446
56829
|
await import_promises16.default.unlink(datasetPath);
|
|
56447
56830
|
await import_promises16.default.rename(tempWorkPath, datasetPath);
|
|
56448
|
-
|
|
56449
|
-
|
|
56450
|
-
this._parseLine = (line, format4, fields) => {
|
|
56451
|
-
return format4 === "CSV" ? LineParser_default._internalParseCSV(line, fields) : LineParser_default._internalParseJSON(line);
|
|
56831
|
+
Affirm_default(outputCount === pivotedDsSchema.rows, `Pivot wrote ${outputCount} row(s) but declared ${pivotedDsSchema.rows}`);
|
|
56832
|
+
return pivotedDsSchema;
|
|
56452
56833
|
};
|
|
56453
56834
|
/**
|
|
56454
56835
|
* Determines if the new record should replace the existing record based on the resolution strategy
|
|
@@ -56475,11 +56856,11 @@ var init_ConsumerExecutor = __esm({
|
|
|
56475
56856
|
return false;
|
|
56476
56857
|
}
|
|
56477
56858
|
};
|
|
56478
|
-
this.processDatasetValidation = async (consumer, datasetPath) => {
|
|
56859
|
+
this.processDatasetValidation = async (consumer, dsSchema, datasetPath) => {
|
|
56860
|
+
Affirm_default(dsSchema, "Invalid dataset schema");
|
|
56479
56861
|
const validations = consumer.validate;
|
|
56480
56862
|
if (!validations || validations.length === 0) return [];
|
|
56481
|
-
const
|
|
56482
|
-
const internalFields = ConsumerManager_default.getExpandedFields(consumer);
|
|
56863
|
+
const parseLine = LineParser_default.datasetParser(dsSchema);
|
|
56483
56864
|
let rowCount = 0;
|
|
56484
56865
|
const seenRows = /* @__PURE__ */ new Set();
|
|
56485
56866
|
const fieldValueSets = /* @__PURE__ */ new Map();
|
|
@@ -56500,7 +56881,7 @@ var init_ConsumerExecutor = __esm({
|
|
|
56500
56881
|
seenRows.add(line);
|
|
56501
56882
|
}
|
|
56502
56883
|
if (uniqueFieldKeys.length > 0) {
|
|
56503
|
-
const record =
|
|
56884
|
+
const record = parseLine(line);
|
|
56504
56885
|
for (const fieldKey of uniqueFieldKeys) {
|
|
56505
56886
|
const valueSet = fieldValueSets.get(fieldKey);
|
|
56506
56887
|
const val = String(record[fieldKey] ?? "");
|
|
@@ -56637,14 +57018,13 @@ var init_ExecutorPerformance = __esm({
|
|
|
56637
57018
|
});
|
|
56638
57019
|
|
|
56639
57020
|
// ../../packages/executors/src/ExecutorWriter.ts
|
|
56640
|
-
var
|
|
57021
|
+
var fs31, ExecutorWriter, ExecutorWriter_default;
|
|
56641
57022
|
var init_ExecutorWriter = __esm({
|
|
56642
57023
|
"../../packages/executors/src/ExecutorWriter.ts"() {
|
|
56643
|
-
|
|
57024
|
+
fs31 = __toESM(require("fs"));
|
|
56644
57025
|
init_src();
|
|
56645
57026
|
init_src3();
|
|
56646
57027
|
init_src10();
|
|
56647
|
-
init_OutputExecutor();
|
|
56648
57028
|
ExecutorWriter = class {
|
|
56649
57029
|
constructor() {
|
|
56650
57030
|
/**
|
|
@@ -56674,7 +57054,8 @@ var init_ExecutorWriter = __esm({
|
|
|
56674
57054
|
Affirm_default(request, "Invalid split request");
|
|
56675
57055
|
Affirm_default(request.sourcePath, "Missing dataset to split");
|
|
56676
57056
|
Affirm_default(request.consumer, "Invalid consumer");
|
|
56677
|
-
const { scope, sourcePath, consumer,
|
|
57057
|
+
const { scope, sourcePath, consumer, dsSchema } = request;
|
|
57058
|
+
Affirm_default(dsSchema, "Missing dataset schema to split against");
|
|
56678
57059
|
const keys = scope.split?.by ?? [];
|
|
56679
57060
|
const maxFileBytes = scope.split?.maxFileSizeGb > 0 ? scope.split.maxFileSizeGb * this.BYTES_PER_GB : null;
|
|
56680
57061
|
if (keys.length === 0 && !maxFileBytes)
|
|
@@ -56689,21 +57070,21 @@ var init_ExecutorWriter = __esm({
|
|
|
56689
57070
|
usedLabels: /* @__PURE__ */ new Set(),
|
|
56690
57071
|
fileCount: 0
|
|
56691
57072
|
};
|
|
56692
|
-
await this._route(pass,
|
|
57073
|
+
await this._route(pass, dsSchema);
|
|
56693
57074
|
if (pass.buckets.size === 0)
|
|
56694
57075
|
return [{ path: sourcePath, label: this.UNSPLIT_LABEL }];
|
|
56695
|
-
await
|
|
57076
|
+
await fs31.promises.unlink(sourcePath);
|
|
56696
57077
|
return this._listFiles(pass);
|
|
56697
57078
|
};
|
|
56698
57079
|
/**
|
|
56699
57080
|
* The pass itself: one line read, one line written, for every line of the dataset.
|
|
56700
57081
|
*/
|
|
56701
|
-
this._route = async (pass,
|
|
56702
|
-
const
|
|
56703
|
-
const readStream =
|
|
57082
|
+
this._route = async (pass, dsSchema) => {
|
|
57083
|
+
const parseLine = pass.keys.length === 0 ? null : LineParser_default.datasetParser(dsSchema);
|
|
57084
|
+
const readStream = fs31.createReadStream(pass.sourcePath);
|
|
56704
57085
|
try {
|
|
56705
57086
|
for await (const line of readLines(readStream)) {
|
|
56706
|
-
const bucket = this._bucketFor(line, pass,
|
|
57087
|
+
const bucket = this._bucketFor(line, pass, parseLine);
|
|
56707
57088
|
const lineBytes = Buffer.byteLength(line) + 1;
|
|
56708
57089
|
if (this._needsRotation(bucket, pass, lineBytes))
|
|
56709
57090
|
await this._rotate(bucket, pass);
|
|
@@ -56723,8 +57104,8 @@ var init_ExecutorWriter = __esm({
|
|
|
56723
57104
|
* own: a shared catch-all would silently merge two different things, and that is invisible until
|
|
56724
57105
|
* it is wrong.
|
|
56725
57106
|
*/
|
|
56726
|
-
this._bucketFor = (line, pass,
|
|
56727
|
-
const bucketKey =
|
|
57107
|
+
this._bucketFor = (line, pass, parseLine) => {
|
|
57108
|
+
const bucketKey = parseLine ? this._bucketKey(line, pass.keys, parseLine) : "";
|
|
56728
57109
|
const existing = pass.buckets.get(bucketKey);
|
|
56729
57110
|
if (existing) return existing;
|
|
56730
57111
|
const bucket = {
|
|
@@ -56738,8 +57119,8 @@ var init_ExecutorWriter = __esm({
|
|
|
56738
57119
|
return bucket;
|
|
56739
57120
|
};
|
|
56740
57121
|
/** The composite value a line buckets on, read off the parsed record. */
|
|
56741
|
-
this._bucketKey = (line, keys,
|
|
56742
|
-
const record =
|
|
57122
|
+
this._bucketKey = (line, keys, parseLine) => {
|
|
57123
|
+
const record = parseLine(line);
|
|
56743
57124
|
return keys.map((key) => {
|
|
56744
57125
|
const value = record[key];
|
|
56745
57126
|
if (value === null || value === void 0) return "_null";
|
|
@@ -56804,7 +57185,7 @@ var init_ExecutorWriter = __esm({
|
|
|
56804
57185
|
}
|
|
56805
57186
|
if (pass.openStreams.length >= Constants_default.defaults.MAX_OPEN_SPLIT_STREAMS)
|
|
56806
57187
|
await this._closeStream(pass.openStreams.shift());
|
|
56807
|
-
bucket.stream =
|
|
57188
|
+
bucket.stream = fs31.createWriteStream(this.getCompletedPath(pass.sourcePath, this._composeLabel(bucket, pass)), { flags: "a" });
|
|
56808
57189
|
pass.openStreams.push(bucket);
|
|
56809
57190
|
return bucket.stream;
|
|
56810
57191
|
};
|
|
@@ -57093,7 +57474,6 @@ var init_LookupResolver = __esm({
|
|
|
57093
57474
|
init_src4();
|
|
57094
57475
|
init_src10();
|
|
57095
57476
|
init_src2();
|
|
57096
|
-
init_OutputExecutor();
|
|
57097
57477
|
LookupResolverClass = class {
|
|
57098
57478
|
constructor() {
|
|
57099
57479
|
// Reuse dicts across sibling consumers within one process; invalidated when the config changes.
|
|
@@ -57116,28 +57496,35 @@ var init_LookupResolver = __esm({
|
|
|
57116
57496
|
Affirm_default(!stack.includes(ref.consumer), `code_lookup cycle detected: ${[...stack, ref.consumer].join(" -> ")}`);
|
|
57117
57497
|
const lookupConsumer = Environment_default.getConsumer(ref.consumer);
|
|
57118
57498
|
Affirm_default(lookupConsumer, `code_lookup references unknown consumer "${ref.consumer}"`);
|
|
57119
|
-
let
|
|
57120
|
-
if (!
|
|
57121
|
-
capturePath = this.getCapturePath(scope, ref.consumer);
|
|
57499
|
+
let capture = captureByConsumer.get(ref.consumer);
|
|
57500
|
+
if (!capture) {
|
|
57501
|
+
const capturePath = this.getCapturePath(scope, ref.consumer);
|
|
57122
57502
|
ExecutorScope_default.ensurePath(capturePath);
|
|
57123
|
-
|
|
57124
|
-
|
|
57503
|
+
capture = {
|
|
57504
|
+
path: capturePath,
|
|
57505
|
+
dsSchema: await this.runLookupConsumer(lookupConsumer, capturePath, stack, request)
|
|
57506
|
+
};
|
|
57507
|
+
captureByConsumer.set(ref.consumer, capture);
|
|
57125
57508
|
}
|
|
57126
|
-
this.cache.set(id, await this.buildDict(
|
|
57509
|
+
this.cache.set(id, await this.buildDict(capture, ref));
|
|
57127
57510
|
}
|
|
57128
57511
|
ExecutorScope_default.ensurePath(outPath);
|
|
57129
57512
|
await import_promises17.default.writeFile(outPath, JSON.stringify(this.cache.get(id)));
|
|
57130
57513
|
manifest.push({ id, consumer: ref.consumer, key: ref.key, value: ref.value, path: import_path26.default.resolve(outPath) });
|
|
57131
57514
|
}
|
|
57132
|
-
for (const
|
|
57133
|
-
await import_promises17.default.rm(
|
|
57515
|
+
for (const capture of captureByConsumer.values())
|
|
57516
|
+
await import_promises17.default.rm(capture.path, { force: true });
|
|
57134
57517
|
return manifest;
|
|
57135
57518
|
};
|
|
57136
|
-
|
|
57519
|
+
/**
|
|
57520
|
+
* Runs the source consumer through the real pipeline, capturing its merged output before
|
|
57521
|
+
* cleanup and returning the shape that run produced. The capture is a verbatim copy of the
|
|
57522
|
+
* dataset, so its schema is the ONLY description of it.
|
|
57523
|
+
*/
|
|
57137
57524
|
this.runLookupConsumer = async (lookupConsumer, capturePath, stack, request) => {
|
|
57138
57525
|
const { default: ExecutorOrchestrator2 } = await Promise.resolve().then(() => (init_ExecutorOrchestrator(), ExecutorOrchestrator_exports));
|
|
57139
57526
|
Logger_default.log(`[lookup] Materializing consumer "${lookupConsumer.name}" (path: ${stack.join(" -> ")})`);
|
|
57140
|
-
await ExecutorOrchestrator2.launch({
|
|
57527
|
+
const result = await ExecutorOrchestrator2.launch({
|
|
57141
57528
|
consumer: lookupConsumer,
|
|
57142
57529
|
details: request.details,
|
|
57143
57530
|
captureMainDatasetTo: capturePath,
|
|
@@ -57145,22 +57532,23 @@ var init_LookupResolver = __esm({
|
|
|
57145
57532
|
_lookupStack: stack,
|
|
57146
57533
|
syntheticProducerInputs: request.syntheticProducerInputs
|
|
57147
57534
|
});
|
|
57535
|
+
Affirm_default(result.dsSchema, `code_lookup source consumer "${lookupConsumer.name}" reported no dataset schema`);
|
|
57536
|
+
return result.dsSchema;
|
|
57148
57537
|
};
|
|
57149
57538
|
// Projects the captured dataset to a { code: value } dict, bounded by MAX_LOOKUP_ROWS.
|
|
57150
|
-
this.buildDict = async (
|
|
57151
|
-
const
|
|
57152
|
-
|
|
57153
|
-
|
|
57154
|
-
|
|
57155
|
-
Affirm_default(finalKeys.includes(ref.value), `code_lookup value column "${ref.value}" is not in the output of consumer "${ref.consumer}"`);
|
|
57539
|
+
this.buildDict = async (capture, ref) => {
|
|
57540
|
+
const columnNames = capture.dsSchema.columns.map((column) => column.name);
|
|
57541
|
+
Affirm_default(columnNames.includes(ref.key), `code_lookup key column "${ref.key}" is not in the output of consumer "${ref.consumer}"`);
|
|
57542
|
+
Affirm_default(columnNames.includes(ref.value), `code_lookup value column "${ref.value}" is not in the output of consumer "${ref.consumer}"`);
|
|
57543
|
+
const parseLine = LineParser_default.datasetParser(capture.dsSchema);
|
|
57156
57544
|
const dict = {};
|
|
57157
57545
|
let rowsRead = 0;
|
|
57158
57546
|
let warnedDup = false;
|
|
57159
|
-
for await (const line of readLines(import_fs25.default.createReadStream(
|
|
57547
|
+
for await (const line of readLines(import_fs25.default.createReadStream(capture.path))) {
|
|
57160
57548
|
if (!line.length) continue;
|
|
57161
57549
|
rowsRead++;
|
|
57162
57550
|
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})`);
|
|
57163
|
-
const record =
|
|
57551
|
+
const record = parseLine(line);
|
|
57164
57552
|
const code = String(record[ref.key]);
|
|
57165
57553
|
if (code in dict && !warnedDup) {
|
|
57166
57554
|
Logger_default.warn(`code_lookup: duplicate key(s) in consumer "${ref.consumer}" for "${ref.key}"; last value wins`);
|
|
@@ -57171,10 +57559,6 @@ var init_LookupResolver = __esm({
|
|
|
57171
57559
|
Logger_default.log(`[lookup] Built table "${ref.consumer}" (${ref.key}->${ref.value}): ${Object.keys(dict).length} entr(ies) from ${rowsRead} row(s)`);
|
|
57172
57560
|
return dict;
|
|
57173
57561
|
};
|
|
57174
|
-
this.parseLine = (line, format4, visibleFields) => {
|
|
57175
|
-
if (format4 === "CSV") return LineParser_default._internalParseCSV(line, visibleFields, ",");
|
|
57176
|
-
return JSON.parse(line);
|
|
57177
|
-
};
|
|
57178
57562
|
this.getCapturePath = (scope, consumerName) => {
|
|
57179
57563
|
const dir = import_path26.default.dirname(ExecutorScope_default.getLookupPath(scope, "x"));
|
|
57180
57564
|
return import_path26.default.join(dir, `${consumerName}.capture`);
|
|
@@ -57208,7 +57592,6 @@ var init_ConsumerInputResolver = __esm({
|
|
|
57208
57592
|
init_src();
|
|
57209
57593
|
init_src3();
|
|
57210
57594
|
init_src4();
|
|
57211
|
-
init_src10();
|
|
57212
57595
|
init_src2();
|
|
57213
57596
|
ConsumerInputResolverClass = class {
|
|
57214
57597
|
constructor() {
|
|
@@ -57233,23 +57616,29 @@ var init_ConsumerInputResolver = __esm({
|
|
|
57233
57616
|
Affirm_default(upstream, `consumer-as-input references unknown consumer "${name}"`);
|
|
57234
57617
|
const capturePath = ExecutorScope_default.getConsumerInputPath(scope, name);
|
|
57235
57618
|
ExecutorScope_default.ensurePath(capturePath);
|
|
57236
|
-
await this.runUpstreamConsumer(upstream, capturePath, stack, request);
|
|
57619
|
+
const dsSchema = await this.runUpstreamConsumer(upstream, capturePath, stack, request);
|
|
57237
57620
|
const { size } = await import_promises18.default.stat(capturePath);
|
|
57238
|
-
this.cache.set(name, { consumer: name, path: import_path27.default.resolve(capturePath), fileSize: size });
|
|
57621
|
+
this.cache.set(name, { consumer: name, path: import_path27.default.resolve(capturePath), fileSize: size, dsSchema });
|
|
57239
57622
|
}
|
|
57240
57623
|
manifest.set(name, this.cache.get(name));
|
|
57241
57624
|
}
|
|
57242
57625
|
return manifest;
|
|
57243
57626
|
};
|
|
57244
|
-
|
|
57245
|
-
|
|
57246
|
-
|
|
57247
|
-
|
|
57248
|
-
|
|
57627
|
+
/**
|
|
57628
|
+
* Builds the in-memory producer the orchestrator reads the JSONL capture through. Its dimensions
|
|
57629
|
+
* are the columns the upstream run REPORTED writing, not the consumer's declared fields, which a
|
|
57630
|
+
* `pivot` leaves describing a shape no longer in the file.
|
|
57631
|
+
*
|
|
57632
|
+
* No `format` is carried over: the capture holds values the upstream run already rendered, so
|
|
57633
|
+
* re-parsing them with that dimension's input format would fail on its own output.
|
|
57634
|
+
*/
|
|
57635
|
+
this.buildSyntheticProducer = (entry) => {
|
|
57636
|
+
Affirm_default(entry, "Invalid consumer input entry");
|
|
57637
|
+
Affirm_default(entry.dsSchema, `Consumer input "${entry.consumer}" carries no dataset schema`);
|
|
57249
57638
|
return {
|
|
57250
|
-
name:
|
|
57251
|
-
source: `__consumer_input__:${
|
|
57252
|
-
dimensions,
|
|
57639
|
+
name: entry.consumer,
|
|
57640
|
+
source: `__consumer_input__:${entry.consumer}`,
|
|
57641
|
+
dimensions: entry.dsSchema.columns.map((column) => ({ name: column.name, type: column.type })),
|
|
57253
57642
|
settings: { fileType: "JSONL" },
|
|
57254
57643
|
_version: 1
|
|
57255
57644
|
};
|
|
@@ -57264,11 +57653,12 @@ var init_ConsumerInputResolver = __esm({
|
|
|
57264
57653
|
}
|
|
57265
57654
|
return names;
|
|
57266
57655
|
};
|
|
57267
|
-
// Runs the upstream consumer through the real pipeline, capturing its
|
|
57656
|
+
// Runs the upstream consumer through the real pipeline, capturing its output as JSONL and
|
|
57657
|
+
// reporting the shape that run produced.
|
|
57268
57658
|
this.runUpstreamConsumer = async (upstream, capturePath, stack, request) => {
|
|
57269
57659
|
const { default: ExecutorOrchestrator2 } = await Promise.resolve().then(() => (init_ExecutorOrchestrator(), ExecutorOrchestrator_exports));
|
|
57270
57660
|
Logger_default.log(`[consumer-input] Materializing consumer "${upstream.name}" (path: ${stack.join(" -> ")})`);
|
|
57271
|
-
await ExecutorOrchestrator2.launch({
|
|
57661
|
+
const result = await ExecutorOrchestrator2.launch({
|
|
57272
57662
|
consumer: upstream,
|
|
57273
57663
|
details: request.details,
|
|
57274
57664
|
captureMainDatasetTo: capturePath,
|
|
@@ -57277,6 +57667,8 @@ var init_ConsumerInputResolver = __esm({
|
|
|
57277
57667
|
_consumerInputStack: stack,
|
|
57278
57668
|
syntheticProducerInputs: request.syntheticProducerInputs
|
|
57279
57669
|
});
|
|
57670
|
+
Affirm_default(result.dsSchema, `consumer-as-input source "${upstream.name}" reported no dataset schema`);
|
|
57671
|
+
return result.dsSchema;
|
|
57280
57672
|
};
|
|
57281
57673
|
this.invalidateCacheIfStale = () => {
|
|
57282
57674
|
const generation = Environment_default._configHash ?? null;
|
|
@@ -57318,6 +57710,7 @@ var init_ExecutorOrchestrator = __esm({
|
|
|
57318
57710
|
init_src();
|
|
57319
57711
|
init_src10();
|
|
57320
57712
|
init_ExecutorWriter();
|
|
57713
|
+
init_DatasetSchema();
|
|
57321
57714
|
import_promises20 = require("stream/promises");
|
|
57322
57715
|
init_src2();
|
|
57323
57716
|
init_ExecutorProgress2();
|
|
@@ -57336,9 +57729,9 @@ var init_ExecutorOrchestrator = __esm({
|
|
|
57336
57729
|
* spawn lazily, on first exec, and a thread that dies mid-run is replaced by another, so this is
|
|
57337
57730
|
* a count of spawns and NOT a concurrency figure (see IWorkerCensus).
|
|
57338
57731
|
*
|
|
57339
|
-
* `workerData` carries the loaded environment
|
|
57340
|
-
*
|
|
57341
|
-
* data goes in the exec argument,
|
|
57732
|
+
* `workerData` carries the loaded environment and the run's dataset schema, so both are cloned
|
|
57733
|
+
* once per THREAD at spawn rather than per chunk. Only run-constant data belongs there; per-task
|
|
57734
|
+
* data goes in the exec argument, cloned per chunk.
|
|
57342
57735
|
*/
|
|
57343
57736
|
this.createPool = (maxWorkers, census, threadData) => {
|
|
57344
57737
|
const options2 = {
|
|
@@ -57369,8 +57762,9 @@ var init_ExecutorOrchestrator = __esm({
|
|
|
57369
57762
|
const tracker = new ExecutorPerformance_default();
|
|
57370
57763
|
const _progress = new ExecutorProgress2_default(logProgress);
|
|
57371
57764
|
const { usageId } = UsageManager_default.startUsage(consumer, details);
|
|
57372
|
-
const scope = { id: usageId, folder: `${consumer.name}_${usageId}`, workersId: [], split: OrchestratorHelper_default.resolveSplit(consumer) };
|
|
57765
|
+
const scope = ExecutorScope_default.open({ id: usageId, folder: `${consumer.name}_${usageId}`, workersId: [], split: OrchestratorHelper_default.resolveSplit(consumer) });
|
|
57373
57766
|
const expandedFields = ConsumerManager_default.getExpandedFields(consumer);
|
|
57767
|
+
let dsSchema = DatasetSchema_default.compile(consumer, expandedFields);
|
|
57374
57768
|
const census = { created: 0, peak: 0 };
|
|
57375
57769
|
try {
|
|
57376
57770
|
const start = performance.now();
|
|
@@ -57405,6 +57799,7 @@ var init_ExecutorOrchestrator = __esm({
|
|
|
57405
57799
|
executorResults.push(...await this.runWorkFiles({
|
|
57406
57800
|
workFiles,
|
|
57407
57801
|
consumer,
|
|
57802
|
+
dsSchema,
|
|
57408
57803
|
scope,
|
|
57409
57804
|
options: options2,
|
|
57410
57805
|
lookups,
|
|
@@ -57419,58 +57814,26 @@ var init_ExecutorOrchestrator = __esm({
|
|
|
57419
57814
|
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))}`);
|
|
57420
57815
|
_progress.update({ phase: "Reconciling work", progress: 0 });
|
|
57421
57816
|
await this.reconcileExecutorThreadsResults(scope, executorResults, tracker);
|
|
57817
|
+
dsSchema = DatasetSchema_default.rewritten(dsSchema, "merge", Algo_default.sum(executorResults.map((x2) => x2.outputCount)));
|
|
57422
57818
|
_progress.update({ phase: "Reconciling work", progress: 1 });
|
|
57423
57819
|
Logger_default.log(`[${usageId}] Reconciled ${executorResults.length} worker result(s)`);
|
|
57424
57820
|
_progress.update({ phase: "Post-processing", progress: 0 });
|
|
57425
|
-
|
|
57426
|
-
if (executorResults.length > 1) {
|
|
57427
|
-
if (consumer.options?.distinct === true) {
|
|
57428
|
-
Logger_default.log(`[${usageId}] Running unified distinct pass across merged workers`);
|
|
57429
|
-
counter = performance.now();
|
|
57430
|
-
const unifiedOutputCount = await ConsumerExecutor_default.processDistinct(ExecutorScope_default.getMainPath(scope));
|
|
57431
|
-
tracker.measure("process-distinct:main", performance.now() - counter);
|
|
57432
|
-
postOperation.totalOutputCount = unifiedOutputCount;
|
|
57433
|
-
Logger_default.log(`[${usageId}] Distinct pass complete: ${unifiedOutputCount} unique rows in ${Math.round(performance.now() - counter)}ms`);
|
|
57434
|
-
}
|
|
57435
|
-
if (consumer.options?.distinctOn) {
|
|
57436
|
-
Logger_default.log(`[${usageId}] Running unified distinctOn pass (${consumer.options.distinctOn})`);
|
|
57437
|
-
counter = performance.now();
|
|
57438
|
-
const unifiedOutputCount = await ConsumerExecutor_default.processDistinctOn(consumer, ExecutorScope_default.getMainPath(scope));
|
|
57439
|
-
tracker.measure("process-distinct-on:main", performance.now() - counter);
|
|
57440
|
-
postOperation.totalOutputCount = unifiedOutputCount;
|
|
57441
|
-
Logger_default.log(`[${usageId}] DistinctOn pass complete: ${unifiedOutputCount} rows in ${Math.round(performance.now() - counter)}ms`);
|
|
57442
|
-
}
|
|
57443
|
-
if (consumer.options?.pivot) {
|
|
57444
|
-
Logger_default.log(`[${usageId}] Running pivot operation`);
|
|
57445
|
-
counter = performance.now();
|
|
57446
|
-
const unifiedOutputCount = await ConsumerExecutor_default.processPivot(consumer, ExecutorScope_default.getMainPath(scope));
|
|
57447
|
-
tracker.measure("process-pivot:main", performance.now() - counter);
|
|
57448
|
-
postOperation.totalOutputCount = unifiedOutputCount;
|
|
57449
|
-
Logger_default.log(`[${usageId}] Pivot complete: ${unifiedOutputCount} rows in ${Math.round(performance.now() - counter)}ms`);
|
|
57450
|
-
}
|
|
57451
|
-
if (ConsumerManager_default.hasLineCountFields(consumer, true)) {
|
|
57452
|
-
Logger_default.log(`[${usageId}] Running unified lineCount pass across merged workers`);
|
|
57453
|
-
counter = performance.now();
|
|
57454
|
-
const processedLineCount = await ConsumerExecutor_default.processLineCount(consumer, ExecutorScope_default.getMainPath(scope), executorResults);
|
|
57455
|
-
tracker.measure("process-line-count:main", performance.now() - counter);
|
|
57456
|
-
Logger_default.log(`[${usageId}] LineCount pass complete: normalized ${processedLineCount} rows in ${Math.round(performance.now() - counter)}ms`);
|
|
57457
|
-
}
|
|
57458
|
-
}
|
|
57821
|
+
dsSchema = await this.runPostPasses({ consumer, dsSchema, scope, executorResults, tracker, usageId });
|
|
57459
57822
|
if (options2?.limit > 0) {
|
|
57460
57823
|
counter = performance.now();
|
|
57461
|
-
const
|
|
57824
|
+
const limited = await ConsumerExecutor_default.processLimit(dsSchema, ExecutorScope_default.getMainPath(scope), options2.limit);
|
|
57462
57825
|
tracker.measure("process-limit:main", performance.now() - counter);
|
|
57463
|
-
if (
|
|
57464
|
-
|
|
57465
|
-
Logger_default.log(`[${usageId}] Record limit applied: dataset cut to ${limitedOutputCount} row(s) in ${Math.round(performance.now() - counter)}ms`);
|
|
57826
|
+
if (limited.writtenBy === "limit") {
|
|
57827
|
+
Logger_default.log(`[${usageId}] Record limit applied: dataset cut to ${limited.rows} row(s) in ${Math.round(performance.now() - counter)}ms`);
|
|
57466
57828
|
} else {
|
|
57467
57829
|
Logger_default.log(`[${usageId}] Record limit of ${options2.limit} not reached \u2014 dataset left whole`);
|
|
57468
57830
|
}
|
|
57831
|
+
dsSchema = limited;
|
|
57469
57832
|
}
|
|
57470
57833
|
if (consumer.validate && consumer.validate.length > 0) {
|
|
57471
57834
|
Logger_default.log(`[${usageId}] Running dataset-level validations`);
|
|
57472
57835
|
counter = performance.now();
|
|
57473
|
-
const validationResults = await ConsumerExecutor_default.processDatasetValidation(consumer, ExecutorScope_default.getMainPath(scope));
|
|
57836
|
+
const validationResults = await ConsumerExecutor_default.processDatasetValidation(consumer, dsSchema, ExecutorScope_default.getMainPath(scope));
|
|
57474
57837
|
tracker.measure("dataset-validation", performance.now() - counter);
|
|
57475
57838
|
for (const result of validationResults) {
|
|
57476
57839
|
if (result.onFail === "fail") {
|
|
@@ -57484,7 +57847,7 @@ var init_ExecutorOrchestrator = __esm({
|
|
|
57484
57847
|
Logger_default.log(`[${usageId}] Dataset validations complete in ${Math.round(performance.now() - counter)}ms`);
|
|
57485
57848
|
}
|
|
57486
57849
|
if (request.captureMainDatasetTo) {
|
|
57487
|
-
await this.captureDataset(
|
|
57850
|
+
await this.captureDataset(dsSchema, ExecutorScope_default.getMainPath(scope), request.captureMainDatasetTo, request.captureFormat);
|
|
57488
57851
|
Logger_default.log(`[${usageId}] Captured dataset to ${request.captureMainDatasetTo}${request.captureFormat ? ` (as ${request.captureFormat})` : ""}`);
|
|
57489
57852
|
}
|
|
57490
57853
|
if (request.skipExport) {
|
|
@@ -57493,8 +57856,8 @@ var init_ExecutorOrchestrator = __esm({
|
|
|
57493
57856
|
const materializeResult = OrchestratorHelper_default.computeFinalResult(tracker, executorResults, usageId, null);
|
|
57494
57857
|
materializeResult.elapsedMS = performance.now() - start;
|
|
57495
57858
|
materializeResult.peakWorkers = census.peak;
|
|
57496
|
-
|
|
57497
|
-
|
|
57859
|
+
materializeResult.outputCount = dsSchema.rows;
|
|
57860
|
+
materializeResult.dsSchema = dsSchema;
|
|
57498
57861
|
UsageManager_default.endUsage(usageId, materializeResult.outputCount, materializeResult);
|
|
57499
57862
|
await Logger_default.flush();
|
|
57500
57863
|
return materializeResult;
|
|
@@ -57504,7 +57867,7 @@ var init_ExecutorOrchestrator = __esm({
|
|
|
57504
57867
|
scope,
|
|
57505
57868
|
sourcePath: ExecutorScope_default.getMainPath(scope),
|
|
57506
57869
|
consumer,
|
|
57507
|
-
|
|
57870
|
+
dsSchema
|
|
57508
57871
|
});
|
|
57509
57872
|
tracker.measure("split-output", performance.now() - counter);
|
|
57510
57873
|
if (scope.split)
|
|
@@ -57513,7 +57876,7 @@ var init_ExecutorOrchestrator = __esm({
|
|
|
57513
57876
|
_progress.update({ phase: "Exporting result", progress: 0 });
|
|
57514
57877
|
counter = performance.now();
|
|
57515
57878
|
Logger_default.info(`[${usageId}] Exporting results to ${consumer.outputs.length} output(s)`);
|
|
57516
|
-
const exportRes = await OutputExecutor_default.exportResult(consumer,
|
|
57879
|
+
const exportRes = await OutputExecutor_default.exportResult(consumer, dsSchema, splitFiles);
|
|
57517
57880
|
tracker.measure("export-result", performance.now() - counter);
|
|
57518
57881
|
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`);
|
|
57519
57882
|
Logger_default.info(`[${usageId}] Export complete in ${Math.round(performance.now() - counter)}ms (key: ${exportRes.key})`);
|
|
@@ -57530,8 +57893,8 @@ var init_ExecutorOrchestrator = __esm({
|
|
|
57530
57893
|
const finalResult = OrchestratorHelper_default.computeFinalResult(tracker, executorResults, usageId, exportRes.key);
|
|
57531
57894
|
finalResult.elapsedMS = performance.now() - start;
|
|
57532
57895
|
finalResult.peakWorkers = census.peak;
|
|
57533
|
-
|
|
57534
|
-
|
|
57896
|
+
finalResult.outputCount = dsSchema.rows;
|
|
57897
|
+
finalResult.dsSchema = dsSchema;
|
|
57535
57898
|
UsageManager_default.endUsage(usageId, finalResult.outputCount, finalResult);
|
|
57536
57899
|
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`);
|
|
57537
57900
|
await Logger_default.flush();
|
|
@@ -57609,7 +57972,10 @@ var init_ExecutorOrchestrator = __esm({
|
|
|
57609
57972
|
`Invalid worker pool size ${poolSize} \u2014 it must come from getParallelWorkerCount(), never from the work list`
|
|
57610
57973
|
);
|
|
57611
57974
|
let phase = performance.now();
|
|
57612
|
-
const pool = this.createPool(poolSize, census, {
|
|
57975
|
+
const pool = this.createPool(poolSize, census, {
|
|
57976
|
+
environment: Environment_default.createSnapshot(),
|
|
57977
|
+
dsSchema: request.dsSchema
|
|
57978
|
+
});
|
|
57613
57979
|
tracker.measure("pool-create", performance.now() - phase);
|
|
57614
57980
|
const context = {
|
|
57615
57981
|
consumer: request.consumer,
|
|
@@ -57832,24 +58198,23 @@ var init_ExecutorOrchestrator = __esm({
|
|
|
57832
58198
|
};
|
|
57833
58199
|
/**
|
|
57834
58200
|
* Copies the merged main.dataset out of the run scope for a materialization run. With
|
|
57835
|
-
* captureFormat 'JSONL' the output is normalized to newline-delimited JSON objects keyed by
|
|
57836
|
-
*
|
|
58201
|
+
* captureFormat 'JSONL' the output is normalized to newline-delimited JSON objects keyed by the
|
|
58202
|
+
* dataset's own column names — self-describing, so a synthetic producer can read it without a
|
|
57837
58203
|
* header row. main.dataset in JSON internal format is already JSONL (verbatim copy); CSV internal
|
|
57838
58204
|
* format has no header row, so each row is reprojected. Without a captureFormat it's a verbatim copy.
|
|
57839
58205
|
*/
|
|
57840
|
-
this.captureDataset = async (
|
|
58206
|
+
this.captureDataset = async (dsSchema, sourcePath, targetPath, captureFormat) => {
|
|
57841
58207
|
await import_promises19.default.mkdir(import_path28.default.dirname(targetPath), { recursive: true });
|
|
57842
|
-
|
|
57843
|
-
if (captureFormat !== "JSONL" || internalFormat !== "CSV") {
|
|
58208
|
+
if (captureFormat !== "JSONL" || dsSchema.encoding !== "CSV") {
|
|
57844
58209
|
await import_promises19.default.copyFile(sourcePath, targetPath);
|
|
57845
58210
|
return;
|
|
57846
58211
|
}
|
|
57847
|
-
const
|
|
58212
|
+
const parseLine = LineParser_default.datasetParser(dsSchema);
|
|
57848
58213
|
const output = import_fs27.default.createWriteStream(targetPath);
|
|
57849
58214
|
try {
|
|
57850
58215
|
for await (const line of readLines(import_fs27.default.createReadStream(sourcePath))) {
|
|
57851
58216
|
if (!line.length) continue;
|
|
57852
|
-
const record =
|
|
58217
|
+
const record = parseLine(line);
|
|
57853
58218
|
if (!output.write(JSON.stringify(record) + "\n"))
|
|
57854
58219
|
await new Promise((resolve) => output.once("drain", resolve));
|
|
57855
58220
|
}
|
|
@@ -57862,7 +58227,7 @@ var init_ExecutorOrchestrator = __esm({
|
|
|
57862
58227
|
}
|
|
57863
58228
|
};
|
|
57864
58229
|
/**
|
|
57865
|
-
* Grabs the remote datasets and
|
|
58230
|
+
* Grabs the remote datasets and stages each one as a local `.extract` to work on
|
|
57866
58231
|
*/
|
|
57867
58232
|
this.readySourceFiles = async (args) => {
|
|
57868
58233
|
const { consumer, scope, consumerInputs, syntheticInputs, options: options2, onProgress } = args;
|
|
@@ -57872,7 +58237,7 @@ var init_ExecutorOrchestrator = __esm({
|
|
|
57872
58237
|
const reportProducer = (fraction) => onProgress?.((i6 + fraction) / consumer.producers.length);
|
|
57873
58238
|
const consumerInput = consumerInputs.get(cProd.name);
|
|
57874
58239
|
if (consumerInput) {
|
|
57875
|
-
const prod2 = ConsumerInputResolver_default.buildSyntheticProducer(
|
|
58240
|
+
const prod2 = ConsumerInputResolver_default.buildSyntheticProducer(consumerInput);
|
|
57876
58241
|
const response2 = { files: [{ fullUri: consumerInput.path, fileSize: consumerInput.fileSize }] };
|
|
57877
58242
|
Logger_default.log(`[${scope.id}] Producer "${cProd.name}" is a consumer input: ${consumerInput.fileSize} bytes (${i6 + 1}/${consumer.producers.length})`);
|
|
57878
58243
|
results.push({ prod: prod2, cProd, response: response2 });
|
|
@@ -58021,6 +58386,49 @@ var init_ExecutorOrchestrator = __esm({
|
|
|
58021
58386
|
}
|
|
58022
58387
|
return mainPath;
|
|
58023
58388
|
};
|
|
58389
|
+
/**
|
|
58390
|
+
* The passes that work across lines, run on the merged dataset. Returns the schema the last
|
|
58391
|
+
* pass to write the file left behind — its `rows` is the run's row count.
|
|
58392
|
+
*
|
|
58393
|
+
* `lineCount` goes FIRST: it walks each chunk's `outputCount` in order to find the worker a line
|
|
58394
|
+
* came from, which only holds while the file still has sum(outputCount) lines in worker order.
|
|
58395
|
+
*/
|
|
58396
|
+
this.runPostPasses = async (request) => {
|
|
58397
|
+
const { consumer, scope, executorResults, tracker, usageId } = request;
|
|
58398
|
+
const datasetPath = ExecutorScope_default.getMainPath(scope);
|
|
58399
|
+
const isMerged = executorResults.length > 1;
|
|
58400
|
+
let dsSchema = request.dsSchema;
|
|
58401
|
+
let counter = 0;
|
|
58402
|
+
if (isMerged && ConsumerManager_default.hasLineCountFields(consumer, true)) {
|
|
58403
|
+
Logger_default.log(`[${usageId}] Running unified lineCount pass across merged workers`);
|
|
58404
|
+
counter = performance.now();
|
|
58405
|
+
dsSchema = await ConsumerExecutor_default.processLineCount(consumer, dsSchema, datasetPath, executorResults);
|
|
58406
|
+
tracker.measure("process-line-count:main", performance.now() - counter);
|
|
58407
|
+
Logger_default.log(`[${usageId}] LineCount pass complete: normalized ${dsSchema.rows} rows in ${Math.round(performance.now() - counter)}ms`);
|
|
58408
|
+
}
|
|
58409
|
+
if (isMerged && consumer.options?.distinct === true) {
|
|
58410
|
+
Logger_default.log(`[${usageId}] Running unified distinct pass across merged workers`);
|
|
58411
|
+
counter = performance.now();
|
|
58412
|
+
dsSchema = await ConsumerExecutor_default.processDistinct(dsSchema, datasetPath);
|
|
58413
|
+
tracker.measure("process-distinct:main", performance.now() - counter);
|
|
58414
|
+
Logger_default.log(`[${usageId}] Distinct pass complete: ${dsSchema.rows} unique rows in ${Math.round(performance.now() - counter)}ms`);
|
|
58415
|
+
}
|
|
58416
|
+
if (isMerged && consumer.options?.distinctOn) {
|
|
58417
|
+
Logger_default.log(`[${usageId}] Running unified distinctOn pass (${consumer.options.distinctOn})`);
|
|
58418
|
+
counter = performance.now();
|
|
58419
|
+
dsSchema = await ConsumerExecutor_default.processDistinctOn(consumer, dsSchema, datasetPath);
|
|
58420
|
+
tracker.measure("process-distinct-on:main", performance.now() - counter);
|
|
58421
|
+
Logger_default.log(`[${usageId}] DistinctOn pass complete: ${dsSchema.rows} rows in ${Math.round(performance.now() - counter)}ms`);
|
|
58422
|
+
}
|
|
58423
|
+
if (consumer.options?.pivot) {
|
|
58424
|
+
Logger_default.log(`[${usageId}] Running pivot operation`);
|
|
58425
|
+
counter = performance.now();
|
|
58426
|
+
dsSchema = await ConsumerExecutor_default.processPivot(consumer, dsSchema, datasetPath);
|
|
58427
|
+
tracker.measure("process-pivot:main", performance.now() - counter);
|
|
58428
|
+
Logger_default.log(`[${usageId}] Pivot complete: ${dsSchema.rows} rows in ${Math.round(performance.now() - counter)}ms`);
|
|
58429
|
+
}
|
|
58430
|
+
return dsSchema;
|
|
58431
|
+
};
|
|
58024
58432
|
this.performCleanupOperations = async (scope, tracker) => {
|
|
58025
58433
|
const start = performance.now();
|
|
58026
58434
|
await ExecutorScope_default.clearScope(scope);
|
|
@@ -58518,6 +58926,7 @@ var LicenceManager_default = LicenceManager;
|
|
|
58518
58926
|
// ../../packages/executors/src/index.ts
|
|
58519
58927
|
init_src();
|
|
58520
58928
|
init_ConsumerExecutor();
|
|
58929
|
+
init_DatasetSchema();
|
|
58521
58930
|
|
|
58522
58931
|
// ../../packages/executors/src/Executor.ts
|
|
58523
58932
|
init_ProducerExecutor();
|
|
@@ -58663,6 +59072,7 @@ init_src2();
|
|
|
58663
59072
|
// ../../packages/executors/src/index.ts
|
|
58664
59073
|
init_ExecutorOrchestrator();
|
|
58665
59074
|
init_ExecutorPerformance();
|
|
59075
|
+
init_ExecutorWriter();
|
|
58666
59076
|
init_OrchestratorHelper();
|
|
58667
59077
|
|
|
58668
59078
|
// ../../packages/executors/src/workers/ExecutorWorkerRunner.ts
|
|
@@ -58670,6 +59080,8 @@ var import_workerpool2 = __toESM(require("workerpool"));
|
|
|
58670
59080
|
var import_dotenv = __toESM(require("dotenv"));
|
|
58671
59081
|
init_src();
|
|
58672
59082
|
init_src4();
|
|
59083
|
+
init_src10();
|
|
59084
|
+
init_DatasetSchema();
|
|
58673
59085
|
init_src2();
|
|
58674
59086
|
import_dotenv.default.configDotenv();
|
|
58675
59087
|
var MODULE_LOADED_AT = performance.timeOrigin + performance.now();
|
|
@@ -58744,8 +59156,7 @@ var SampleRunnerClass = class {
|
|
|
58744
59156
|
captureMainDatasetTo: capturePath,
|
|
58745
59157
|
captureFormat: "JSONL"
|
|
58746
59158
|
});
|
|
58747
|
-
const
|
|
58748
|
-
const records = captured.map((record) => this._inFieldOrder(consumer, record));
|
|
59159
|
+
const records = await this._readCapture(capturePath, size);
|
|
58749
59160
|
Logger_default.log(`[sample] Consumer "${consumer.name}": ${records.length} row(s) from ${result.inputCount} input line(s)`);
|
|
58750
59161
|
return {
|
|
58751
59162
|
kind: "consumer",
|
|
@@ -58793,28 +59204,6 @@ var SampleRunnerClass = class {
|
|
|
58793
59204
|
return this._inputProducers(nested, seen);
|
|
58794
59205
|
});
|
|
58795
59206
|
};
|
|
58796
|
-
/**
|
|
58797
|
-
* One row's keys in the order the consumer DECLARES its fields.
|
|
58798
|
-
*
|
|
58799
|
-
* A record comes out of a worker keyed in the order the producer's dimensions were read, which is
|
|
58800
|
-
* the source's layout and not the consumer's — so a consumer declaring country, plan, is_active
|
|
58801
|
-
* over a producer that stores is_active first would be shown its own columns in someone else's
|
|
58802
|
-
* order. Invisible in an exported JSON object; the whole shape of the table here.
|
|
58803
|
-
*
|
|
58804
|
-
* Anything the record carries that the field list does not name is appended rather than dropped:
|
|
58805
|
-
* a column that turns up unexpectedly is exactly the one worth seeing.
|
|
58806
|
-
*/
|
|
58807
|
-
this._inFieldOrder = (consumer, record) => {
|
|
58808
|
-
const declared = ConsumerManager_default.getExpandedFields(consumer).filter((field) => !field.cField.hidden).map((field) => field.finalKey);
|
|
58809
|
-
const ordered = {};
|
|
58810
|
-
for (const key of declared) {
|
|
58811
|
-
if (key in record) ordered[key] = record[key];
|
|
58812
|
-
}
|
|
58813
|
-
for (const key of Object.keys(record)) {
|
|
58814
|
-
if (!(key in ordered)) ordered[key] = record[key];
|
|
58815
|
-
}
|
|
58816
|
-
return ordered;
|
|
58817
|
-
};
|
|
58818
59207
|
/** The captured dataset, which `captureFormat: 'JSONL'` guarantees is one JSON object per line. */
|
|
58819
59208
|
this._readCapture = async (capturePath, size) => {
|
|
58820
59209
|
if (!import_fs28.default.existsSync(capturePath)) return [];
|
|
@@ -59359,7 +59748,7 @@ var create_producer = async (name) => {
|
|
|
59359
59748
|
console.log(import_chalk12.default.green(`\u2705 Created producer config at ${producerPath}`));
|
|
59360
59749
|
console.log(import_chalk12.default.blue("Remember to:"));
|
|
59361
59750
|
console.log(import_chalk12.default.blue("1. Set the correct source name"));
|
|
59362
|
-
console.log(import_chalk12.default.blue(`2. Configure the dimensions
|
|
59751
|
+
console.log(import_chalk12.default.blue(`2. Configure the dimensions, or run "remora discover ${name} --replace" to`));
|
|
59363
59752
|
console.log(import_chalk12.default.blue(" write them from the source over the placeholders this template ships with"));
|
|
59364
59753
|
console.log(import_chalk12.default.blue("3. Update the settings as needed"));
|
|
59365
59754
|
} catch (error) {
|
|
@@ -59397,13 +59786,6 @@ var create_consumer = async (name, producerName) => {
|
|
|
59397
59786
|
description: dim.description,
|
|
59398
59787
|
from: producerName
|
|
59399
59788
|
}));
|
|
59400
|
-
if (producerConfig.measures && producerConfig.measures.length > 0) {
|
|
59401
|
-
defaultConsumer.fields.push(...producerConfig.measures.map((measure) => ({
|
|
59402
|
-
key: measure.name,
|
|
59403
|
-
description: measure.description,
|
|
59404
|
-
from: producerName
|
|
59405
|
-
})));
|
|
59406
|
-
}
|
|
59407
59789
|
defaultConsumer.description = producerConfig.description || `Consumer for ${producerName} data`;
|
|
59408
59790
|
defaultConsumer.schema = void 0;
|
|
59409
59791
|
defaultConsumer.filters = void 0;
|