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