@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/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 fs39 = require("fs");
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
- fs39.open(options2.file, "a+", "0644", (err2, fd) => {
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
- fs39.close(fd, noop);
8526
+ fs40.close(fd, noop);
8527
8527
  return;
8528
8528
  }
8529
- return fs39.read(fd, buffer, 0, buffer.length, pos, (error, bytes) => {
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 fs39 = require("fs");
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 = fs39.createReadStream(file, {
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
- fs39.stat(fullpath, (err2, stat) => {
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 = fs39.createWriteStream(fullpath, this.options).on("error", (err2) => debug(err2)).on("close", () => debug("close", dest.path, dest.bytesWritten)).on("open", () => {
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
- fs39.unlink(target, callback);
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
- fs39.exists(tmppath, (exists) => {
9152
+ fs40.exists(tmppath, (exists) => {
9153
9153
  if (!exists) {
9154
9154
  return cb(null);
9155
9155
  }
9156
9156
  fileName = `${basename}${i6}${ext}${isZipped}`;
9157
- fs39.rename(tmppath, path30.join(this.dirname, fileName), cb);
9157
+ fs40.rename(tmppath, path30.join(this.dirname, fileName), cb);
9158
9158
  });
9159
9159
  }.bind(this, x2));
9160
9160
  }
9161
9161
  asyncSeries(tasks, () => {
9162
- fs39.rename(
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
- fs39.access(src, fs39.F_OK, (err2) => {
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 = fs39.createReadStream(src);
9184
- var out = fs39.createWriteStream(dest);
9183
+ var inp = fs40.createReadStream(src);
9184
+ var out = fs40.createWriteStream(dest);
9185
9185
  out.on("finish", () => {
9186
- fs39.unlink(src, callback);
9186
+ fs40.unlink(src, callback);
9187
9187
  });
9188
9188
  inp.pipe(gzip).pipe(out);
9189
9189
  });
9190
9190
  }
9191
9191
  _createLogDirIfNotExist(dirPath) {
9192
- if (!fs39.existsSync(dirPath)) {
9193
- fs39.mkdirSync(dirPath, { recursive: true });
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.3.0",
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}.dataset`
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 openScopes = this.getOpenScopes();
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 openScopes = ExecutorScope_default.getOpenScopes();
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
- const dims = producer.dimensions.map((x2) => ({
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
- const duplicatesOutputs = Algo_default.duplicatesObject(consumer.outputs, "format");
13092
- if (duplicatesOutputs.length > 0) {
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 positional = (consumer.outputs ?? []).filter((x2) => positionalFormats.includes(x2.format.toUpperCase()));
13194
- if (positional.length === 0) return errors;
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 ${positional.map((x2) => x2.format.toUpperCase()).join("/")} output does not write \u2014 a hidden field can't be split on in a positional format.`);
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 a
13202
- * record carries its own field names, and it is applied when the record is serialized — one record
13203
- * shape for every output of the consumer — so a positional sibling output would silently lose its
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 and an API response is not a file, so the flag means
13224
- * nothing there — rejected rather than ignored, so a config that asks for a header always gets
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 positionalFormats = ["CSV", "TXT", "PARQUET"];
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 (!positionalFormats.includes(format4))
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 fs39 = require("fs");
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
- fs39.read(self2.fd, buffer, offset, length, position, function(err2, bytesRead, buffer2) {
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
- fs39.close(self2.fd, onCloseDone);
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
- fs39.read(self2.context.fd, buffer, 0, toRead, self2.pos, function(err2, bytesRead) {
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 fs39 = require("fs");
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
- fs39.open(path30, "r", function(err2, fd) {
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) fs39.close(fd, defaultCallback);
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
- fs39.fstat(fd, function(err2, stats) {
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 dimension: the type cast, plus what a MISSING value means for it.
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
- * Used ONLY to parse internal records (inside the .dataset) since I know they are already prepared in the correct way
48114
- */
48115
- this._internalParseCSV = (line, fields, delimiter) => {
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._internalParseJSON = (line) => {
48128
- return JSON.parse(line);
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 ? "%.dataset" : import_path18.default.basename(firstFile),
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, measure: col.measure });
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 measure = (producer.measures ?? []).find((x2) => x2.name === field.key);
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 || measure) return { path: [step] };
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, dimensions then measures, in declared order. */
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 output decisions a consumer makes once — the internal format, whether nulls are omitted,
55869
- * which fields are visible — lifted off the per-record path. `outputRecord` used to re-derive
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, fields) => {
56210
+ this.compileOutput = (consumer, dsSchema) => {
55873
56211
  Affirm_default(consumer, "Invalid consumer");
55874
- Affirm_default(fields, "Invalid consumer fields");
56212
+ Affirm_default(dsSchema, "Invalid dataset schema");
55875
56213
  return {
55876
- format: this._getInternalRecordFormat(consumer),
56214
+ format: dsSchema.encoding,
55877
56215
  delimiter: ",",
55878
- visibleKeys: fields.filter((x2) => !x2.cField.hidden).map((x2) => x2.finalKey),
55879
- hiddenKeys: fields.filter((x2) => x2.cField.hidden).map((x2) => x2.finalKey),
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 += CSVParser_default.escapeField(record[visibleKeys[index]]?.toString() ?? "");
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
- for (const key of plan.hiddenKeys)
55905
- delete record[key];
55906
- if (plan.omitNull) {
55907
- for (const key of Object.keys(record)) {
55908
- if (record[key] === null || record[key] === void 0)
55909
- delete record[key];
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(record);
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 every configured destination, one call per file the split pass
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, fields, splitFiles) => {
56282
+ this.exportResult = async (consumer, dsSchema, splitFiles) => {
55931
56283
  Affirm_default.hasItems(splitFiles, `Consumer "${consumer.name}" has no output file to export`);
55932
- const internalFormat = this._getInternalRecordFormat(consumer);
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 output of consumer.outputs) {
55935
- const destination = Environment_default.getSource(output.exportDestination);
55936
- const driver = await DriverFactory_default.instantiateDestination(destination);
55937
- const destinationName = this._composeFileName(consumer, output, this._getExtension(output));
55938
- Logger_default.info(`Exporting consumer "${consumer.name}" to "${output.exportDestination}" as ${output.format} (${destinationName}, ${splitFiles.length} file(s))`);
55939
- const header = this._composeHeader(output, fields);
55940
- for (const { path: startingPath, label } of splitFiles) {
55941
- const destinationPath = this.getCompletedPath(destinationName, label);
55942
- if (output.format === internalFormat) {
55943
- results.push(await driver.move(startingPath, destinationPath, header));
55944
- } else {
55945
- switch (output.format) {
55946
- case "CSV":
55947
- results.push(
55948
- await driver.transformAndMove(startingPath, (line) => {
55949
- const parsed = JSON.parse(line);
55950
- return Object.keys(parsed).map((x2) => `"${parsed[x2]}"`).join(",");
55951
- }, destinationPath, header)
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 are the exported ones (`finalKey`, i.e. the alias where one is set), since a reader
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, fields) => {
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
- const names = fields.filter((x2) => !x2.cField.hidden).map((x2) => x2.finalKey);
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
- return output.format === "CSV" ? "csv" : output.format === "JSON" ? "jsonl" : "txt";
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
- init_src10();
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
- this.processLineCount = async (consumer, datasetPath, executorResults) => {
56177
- const internalRecordFormat = OutputExecutor_default._getInternalRecordFormat(consumer);
56178
- const internalFields = ConsumerManager_default.getExpandedFields(consumer);
56179
- const outputPlan = OutputExecutor_default.compileOutput(consumer, internalFields);
56180
- const lineCountFields = ConsumerManager_default.getLineCountFieldsFromExpanded(internalFields, true);
56181
- if (lineCountFields.length === 0)
56182
- return 0;
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 = this._parseLine(line, internalRecordFormat, internalFields);
56205
- for (const field of lineCountFields) {
56206
- const currentValue = Number(record[field.finalKey]);
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[field.finalKey] = currentValue + rowOffset;
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 row count it left
56235
- * behind — or `null` when the dataset was already within the limit and nothing was touched.
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 null;
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 null;
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 internalRecordFormat = OutputExecutor_default._getInternalRecordFormat(consumer);
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 = internalRecordFormat === "CSV" ? LineParser_default._internalParseCSV(line, internalFields) : LineParser_default._internalParseJSON(line);
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
- this.processPivot = async (consumer, datasetPath) => {
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 internalRecordFormat = OutputExecutor_default._getInternalRecordFormat(consumer);
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 = this._parseLine(line, internalRecordFormat, internalFields);
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 = this._parseLine(line, internalRecordFormat, internalFields);
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 pivotedFields = [
56395
- ...rowKeys.map((k7) => ({ cField: { key: k7 }, finalKey: k7 })),
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
- return outputCount;
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 internalRecordFormat = OutputExecutor_default._getInternalRecordFormat(consumer);
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 = internalRecordFormat === "CSV" ? LineParser_default._internalParseCSV(line, internalFields) : LineParser_default._internalParseJSON(line);
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 fs30, ExecutorWriter, ExecutorWriter_default;
57021
+ var fs31, ExecutorWriter, ExecutorWriter_default;
56641
57022
  var init_ExecutorWriter = __esm({
56642
57023
  "../../packages/executors/src/ExecutorWriter.ts"() {
56643
- fs30 = __toESM(require("fs"));
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, fields } = request;
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, fields);
57073
+ await this._route(pass, dsSchema);
56693
57074
  if (pass.buckets.size === 0)
56694
57075
  return [{ path: sourcePath, label: this.UNSPLIT_LABEL }];
56695
- await fs30.promises.unlink(sourcePath);
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, fields) => {
56702
- const internalFormat = OutputExecutor_default._getInternalRecordFormat(pass.consumer);
56703
- const readStream = fs30.createReadStream(pass.sourcePath);
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, internalFormat, fields);
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, internalFormat, fields) => {
56727
- const bucketKey = pass.keys.length === 0 ? "" : this._bucketKey(line, pass.keys, internalFormat, fields);
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, internalFormat, fields) => {
56742
- const record = internalFormat === "CSV" ? LineParser_default._internalParseCSV(line, fields) : LineParser_default._internalParseJSON(line);
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 = fs30.createWriteStream(this.getCompletedPath(pass.sourcePath, this._composeLabel(bucket, pass)), { flags: "a" });
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 capturePath = captureByConsumer.get(ref.consumer);
57120
- if (!capturePath) {
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
- await this.runLookupConsumer(lookupConsumer, capturePath, stack, request);
57124
- captureByConsumer.set(ref.consumer, capturePath);
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(lookupConsumer, capturePath, ref));
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 capturePath of captureByConsumer.values())
57133
- await import_promises17.default.rm(capturePath, { force: true });
57515
+ for (const capture of captureByConsumer.values())
57516
+ await import_promises17.default.rm(capture.path, { force: true });
57134
57517
  return manifest;
57135
57518
  };
57136
- // Runs the source consumer through the real pipeline, capturing its merged output before cleanup.
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 (lookupConsumer, capturePath, ref) => {
57151
- const format4 = OutputExecutor_default._getInternalRecordFormat(lookupConsumer);
57152
- const visibleFields = ConsumerManager_default.getExpandedFields(lookupConsumer).filter((f7) => !f7.cField.hidden);
57153
- const finalKeys = visibleFields.map((f7) => f7.finalKey);
57154
- Affirm_default(finalKeys.includes(ref.key), `code_lookup key column "${ref.key}" is not in the output of consumer "${ref.consumer}"`);
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(capturePath))) {
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 = this.parseLine(line, format4, visibleFields);
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
- // Builds the in-memory producer the orchestrator reads the JSONL capture through. Its dimensions
57245
- // are the upstream consumer's visible output fields, matching the keys written into the capture.
57246
- this.buildSyntheticProducer = (upstreamConsumer) => {
57247
- Affirm_default(upstreamConsumer, "Invalid upstream consumer");
57248
- const dimensions = ConsumerManager_default.getExpandedFields(upstreamConsumer).filter((field) => !field.cField.hidden).map((field) => ({ name: field.finalKey, type: field.dimension?.type ?? "string" }));
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: upstreamConsumer.name,
57251
- source: `__consumer_input__:${upstreamConsumer.name}`,
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 full output as JSONL.
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, so it is cloned once per THREAD at spawn rather
57340
- * than read and validated from disk once per chunk. Only run-constant data belongs there; per-task
57341
- * data goes in the exec argument, which is cloned again for every chunk.
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
- const postOperation = { totalOutputCount: null };
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 limitedOutputCount = await ConsumerExecutor_default.processLimit(ExecutorScope_default.getMainPath(scope), options2.limit);
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 (Algo_default.hasVal(limitedOutputCount)) {
57464
- postOperation.totalOutputCount = limitedOutputCount;
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(consumer, expandedFields, ExecutorScope_default.getMainPath(scope), request.captureMainDatasetTo, request.captureFormat);
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
- if (Algo_default.hasVal(postOperation.totalOutputCount))
57497
- materializeResult.outputCount = postOperation.totalOutputCount;
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
- fields: expandedFields
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, expandedFields, splitFiles);
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
- if (Algo_default.hasVal(postOperation.totalOutputCount))
57534
- finalResult.outputCount = postOperation.totalOutputCount;
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, { environment: Environment_default.createSnapshot() });
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
- * each visible field's finalKey — self-describing, so a synthetic producer can read it without a
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 (consumer, fields, sourcePath, targetPath, captureFormat) => {
58206
+ this.captureDataset = async (dsSchema, sourcePath, targetPath, captureFormat) => {
57841
58207
  await import_promises19.default.mkdir(import_path28.default.dirname(targetPath), { recursive: true });
57842
- const internalFormat = OutputExecutor_default._getInternalRecordFormat(consumer);
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 visibleFields = fields.filter((field) => !field.cField.hidden);
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 = LineParser_default._internalParseCSV(line, visibleFields, ",");
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 creates an internal .dataset to work on
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(Environment_default.getConsumer(cProd.name));
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 captured = await this._readCapture(capturePath, size);
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 and measures, or run "remora discover ${name} --replace" to`));
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;