@forzalabs/remora 1.2.10 → 1.2.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -6,6 +6,13 @@ The format is based on Keep a Changelog, and this project adheres to Semantic Ve
6
6
 
7
7
  ## Unreleased
8
8
 
9
+ ## V 1.2.11 - 2026-06-30
10
+
11
+ ### Added
12
+ - Added `lineCount` as a consumer field property, allowing consumers to emit a consistent 1-based source line number even when files are processed across multiple workers
13
+ - Added a dedicated `ConsumerExecutor` post-processing pass to normalize `lineCount` values after worker merge, with tracked execution timing similar to other dataset-level steps
14
+ - Added `lineCount` test coverage to canary and CLI consumer configurations for easier verification of the feature
15
+
9
16
  ## V 1.2.2 - 2026-04-10
10
17
 
11
18
  ### Added
package/index.js CHANGED
@@ -18751,7 +18751,7 @@ var import_promises = __toESM(require("fs/promises"), 1);
18751
18751
 
18752
18752
  // ../../packages/constants/src/Constants.ts
18753
18753
  var CONSTANTS = {
18754
- cliVersion: "1.2.10",
18754
+ cliVersion: "1.2.11",
18755
18755
  backendVersion: 1,
18756
18756
  backendPort: 5088,
18757
18757
  workerVersion: 2,
@@ -19191,7 +19191,16 @@ var ValidatorClass = class {
19191
19191
  }
19192
19192
  const allAvailableFields = [...availableFieldsByProducer.values()].flat();
19193
19193
  for (const field of consumer.fields) {
19194
- if (field.key === "*" || field.fixed || field.copyFrom) continue;
19194
+ const syntheticFieldModes = [field.fixed === true, !!field.copyFrom, field.lineCount === true].filter(Boolean);
19195
+ if (syntheticFieldModes.length > 1)
19196
+ errors.push(`Field "${field.alias ?? field.key}" in consumer "${consumer.name}" can only use one synthetic field mode among "fixed", "copyFrom", and "lineCount".`);
19197
+ if (field.lineCount && field.key === "*")
19198
+ errors.push(`Field "${field.alias ?? field.key}" in consumer "${consumer.name}" cannot use "lineCount" with the wildcard key "*".`);
19199
+ if (field.lineCount && field.transform)
19200
+ errors.push(`Field "${field.alias ?? field.key}" in consumer "${consumer.name}" cannot use "transform" together with "lineCount".`);
19201
+ if (field.lineCount && field.validate && field.validate.length > 0)
19202
+ errors.push(`Field "${field.alias ?? field.key}" in consumer "${consumer.name}" cannot use "validate" together with "lineCount".`);
19203
+ if (field.key === "*" || field.fixed || field.copyFrom || field.lineCount) continue;
19195
19204
  if (field.from) {
19196
19205
  const producerFields = availableFieldsByProducer.get(field.from);
19197
19206
  if (producerFields && !producerFields.includes(field.key))
@@ -19212,6 +19221,8 @@ var ValidatorClass = class {
19212
19221
  const found = precedingFields.find((f) => (f.alias ?? f.key) === field.copyFrom);
19213
19222
  if (!found)
19214
19223
  errors.push(`Field "${field.alias ?? field.key}" uses copyFrom "${field.copyFrom}" but no field with that name/alias exists before it in consumer "${consumer.name}".`);
19224
+ else if (found.lineCount)
19225
+ errors.push(`Field "${field.alias ?? field.key}" in consumer "${consumer.name}" cannot use copyFrom on a field with "lineCount" because line counts are normalized after worker merge.`);
19215
19226
  }
19216
19227
  if (consumer.filters && consumer.filters.length > 0) {
19217
19228
  if (consumer.filters.some((x) => x.sql && x.rule))
@@ -19232,10 +19243,14 @@ var ValidatorClass = class {
19232
19243
  const missingRules = rulesWithMatchingFields.filter((x) => !x.match);
19233
19244
  errors.push(`Filter(s) on member(s) "${missingRules.map((x) => x.rule.rule.member).join(", ")}" is invalid since the member specified is not present in the consumer. Check the member value or add the missing field to the consumer.`);
19234
19245
  }
19246
+ const lineCountRuleMembers = rulesWithMatchingFields.filter((x) => x.match?.lineCount);
19247
+ if (lineCountRuleMembers.length > 0)
19248
+ errors.push(`Consumer "${consumer.name}" cannot use rule-based filters on field(s) with "lineCount": ${lineCountRuleMembers.map((x) => `"${x.rule.rule.member}"`).join(", ")}.`);
19235
19249
  }
19236
19250
  const validateTransformations = (fields) => {
19237
19251
  const errors2 = [];
19238
19252
  const trxsFields = fields.filter((x) => x.transform);
19253
+ const lineCountFieldNames = new Set(fields.filter((x) => x.lineCount).map((x) => x.alias ?? x.key));
19239
19254
  for (const field of trxsFields) {
19240
19255
  const trxToValidate = [];
19241
19256
  if (Array.isArray(field.transform))
@@ -19243,6 +19258,16 @@ var ValidatorClass = class {
19243
19258
  else
19244
19259
  trxToValidate.push(field.transform);
19245
19260
  for (const trans of trxToValidate) {
19261
+ if ("multiplyBy" in trans) {
19262
+ const invalidFields = trans.multiplyBy.fields.filter((x) => lineCountFieldNames.has(x));
19263
+ if (invalidFields.length > 0)
19264
+ errors2.push(`Transformation on field "${field.alias ?? field.key}" cannot reference lineCount field(s) in multiplyBy: ${invalidFields.map((x) => `"${x}"`).join(", ")}.`);
19265
+ }
19266
+ if ("addBy" in trans) {
19267
+ const invalidFields = trans.addBy.fields.filter((x) => lineCountFieldNames.has(x));
19268
+ if (invalidFields.length > 0)
19269
+ errors2.push(`Transformation on field "${field.alias ?? field.key}" cannot reference lineCount field(s) in addBy: ${invalidFields.map((x) => `"${x}"`).join(", ")}.`);
19270
+ }
19246
19271
  if ("combine_fields" in trans) {
19247
19272
  const { combine_fields } = trans;
19248
19273
  if (!combine_fields.fields || combine_fields.fields.length === 0)
@@ -19250,6 +19275,9 @@ var ValidatorClass = class {
19250
19275
  const missingFieldsInConsumer = combine_fields.fields.map((x) => ({ field: x, found: fields.find((k) => (k.alias ?? k.key) === x) })).filter((x) => !x.found);
19251
19276
  if (missingFieldsInConsumer.length > 0)
19252
19277
  errors2.push(`The requested field(s) for a transformation is missing in the consumer -> missing field(s): "${missingFieldsInConsumer.map((x) => x.field).join(", ")}"; field transformation: "${field.alias ?? field.key}";`);
19278
+ const invalidFields = combine_fields.fields.filter((x) => lineCountFieldNames.has(x));
19279
+ if (invalidFields.length > 0)
19280
+ errors2.push(`Transformation on field "${field.alias ?? field.key}" cannot reference lineCount field(s) in combine_fields: ${invalidFields.map((x) => `"${x}"`).join(", ")}.`);
19253
19281
  }
19254
19282
  }
19255
19283
  }
@@ -19272,7 +19300,7 @@ var ValidatorClass = class {
19272
19300
  errors.push(`The export destination "${output.exportDestination}" was not found in the sources.`);
19273
19301
  }
19274
19302
  }
19275
- const dimensionFields = consumer.fields.filter((x) => x.key !== "*" && !x.fixed && !x.copyFrom);
19303
+ const dimensionFields = consumer.fields.filter((x) => x.key !== "*" && !x.fixed && !x.copyFrom && !x.lineCount);
19276
19304
  const dimensionKeys = dimensionFields.map((x) => `${x.from ?? "_default_"}::${x.key}`);
19277
19305
  const duplicateDimensionKeys = dimensionKeys.filter((k, i) => dimensionKeys.indexOf(k) !== i);
19278
19306
  if (duplicateDimensionKeys.length > 0) {
@@ -23459,6 +23487,25 @@ var ConsumerManagerClass = class {
23459
23487
  const expandedFields = convertedFields.flatMap((x) => this.expandField(consumer, x, availableColumns));
23460
23488
  return expandedFields;
23461
23489
  };
23490
+ this.getLineCountFields = (consumer, visibleOnly = false) => {
23491
+ Affirm_default(consumer, "Invalid consumer");
23492
+ return this.getLineCountFieldsFromExpanded(this.getExpandedFields(consumer), visibleOnly);
23493
+ };
23494
+ this.getLineCountFieldsFromExpanded = (fields, visibleOnly = false) => {
23495
+ Affirm_default(fields, "Invalid consumer fields");
23496
+ return fields.filter((field) => this.isLineCountField(field.cField) && (!visibleOnly || !field.cField.hidden));
23497
+ };
23498
+ this.hasLineCountFields = (consumer, visibleOnly = false) => {
23499
+ return this.getLineCountFields(consumer, visibleOnly).length > 0;
23500
+ };
23501
+ this.isSyntheticField = (field) => {
23502
+ Affirm_default(field, "Invalid field");
23503
+ return field.fixed === true || !!field.copyFrom || this.isLineCountField(field);
23504
+ };
23505
+ this.isLineCountField = (field) => {
23506
+ Affirm_default(field, "Invalid field");
23507
+ return field.lineCount === true;
23508
+ };
23462
23509
  this.getAvailableColumns = (consumer) => {
23463
23510
  return ResourcesUtils_default.getAvailableColumns(consumer);
23464
23511
  };
@@ -23511,7 +23558,7 @@ var ConsumerManagerClass = class {
23511
23558
  column = columns.find((x) => x.owner === field.from && x.nameInProducer === field.key);
23512
23559
  } else if (consumer.producers.length === 1 && !field.from) {
23513
23560
  column = columns.find((x) => x.nameInProducer === field.key);
23514
- } else if (!field.fixed && !field.copyFrom) {
23561
+ } else if (!this.isSyntheticField(field)) {
23515
23562
  const matches = columns.filter((x) => x.nameInProducer === field.key);
23516
23563
  Affirm_default(matches.length > 0, `Consumer "${consumer.name}" misconfiguration: the field "${field.key}" is not found in any of the included producers (${consumer.producers.map((x) => x.name).join(", ")})`);
23517
23564
  if (matches.length === 1) {
@@ -23522,14 +23569,14 @@ var ConsumerManagerClass = class {
23522
23569
  column = matches[0];
23523
23570
  }
23524
23571
  if (!column) {
23525
- if (field.fixed === true && Algo_default.hasVal(field.default) || field.copyFrom) {
23572
+ if (field.fixed === true && Algo_default.hasVal(field.default) || this.isSyntheticField(field)) {
23526
23573
  column = {
23527
23574
  aliasInProducer: field.key,
23528
23575
  nameInProducer: field.alias ?? field.key,
23529
23576
  consumerAlias: field.alias ?? field.key,
23530
23577
  consumerKey: field.key,
23531
23578
  owner: field.from,
23532
- dimension: { name: field.key, type: typeof field.default }
23579
+ dimension: { name: field.key, type: this.isLineCountField(field) ? "number" : typeof field.default }
23533
23580
  };
23534
23581
  }
23535
23582
  }
@@ -24841,16 +24888,23 @@ var ConsumerExecutorClass = class {
24841
24888
  for (const field of fields) {
24842
24889
  const { cField } = field;
24843
24890
  const fieldKey = cField.alias ?? cField.key;
24891
+ if (cField.fixed && Algo_default.hasVal(cField.default)) {
24892
+ record[fieldKey] = cField.default;
24893
+ continue;
24894
+ }
24895
+ if (ConsumerManager_default.isLineCountField(cField)) {
24896
+ record[fieldKey] = recordIndex + 1;
24897
+ continue;
24898
+ }
24899
+ if (cField.copyFrom) {
24900
+ record[fieldKey] = record[cField.copyFrom];
24901
+ continue;
24902
+ }
24844
24903
  let dimension;
24845
24904
  try {
24846
24905
  dimension = dimensions.find((x) => x.name === cField.key);
24847
24906
  if (!dimension) {
24848
- if (cField.fixed && Algo_default.hasVal(cField.default))
24849
- record[fieldKey] = cField.default;
24850
- else if (cField.copyFrom)
24851
- record[fieldKey] = record[cField.copyFrom];
24852
- else
24853
- throw new Error(`The requested field "${cField.key}" from the consumer is not present in the underlying producer "${producer.name}" (${dimensions.map((x) => x.name).join(", ")})`);
24907
+ throw new Error(`The requested field "${cField.key}" from the consumer is not present in the underlying producer "${producer.name}" (${dimensions.map((x) => x.name).join(", ")})`);
24854
24908
  }
24855
24909
  } catch (error) {
24856
24910
  const err2 = new Error(`Resolving dimension for field "${fieldKey}" of producer "${producer.name}" failed (index: ${recordIndex}): ${error.message}`, { cause: error });
@@ -24963,6 +25017,64 @@ var ConsumerExecutorClass = class {
24963
25017
  }
24964
25018
  return record;
24965
25019
  };
25020
+ this.processLineCount = async (consumer, datasetPath, executorResults) => {
25021
+ const internalRecordFormat = OutputExecutor_default._getInternalRecordFormat(consumer);
25022
+ const internalFields = ConsumerManager_default.getExpandedFields(consumer);
25023
+ const lineCountFields = ConsumerManager_default.getLineCountFieldsFromExpanded(internalFields, true);
25024
+ if (lineCountFields.length === 0)
25025
+ return 0;
25026
+ const tempWorkPath = datasetPath + "_tmp";
25027
+ const reader = import_fs12.default.createReadStream(datasetPath);
25028
+ const lineReader = import_readline6.default.createInterface({ input: reader, crlfDelay: Infinity });
25029
+ const writer = import_fs12.default.createWriteStream(tempWorkPath);
25030
+ const waitForDrain = () => new Promise((resolve) => writer.once("drain", resolve));
25031
+ let workerIndex = 0;
25032
+ let currentWorker = executorResults[workerIndex] ?? null;
25033
+ let remainingLinesInWorker = currentWorker?.outputCount ?? 0;
25034
+ let processedLineCount = 0;
25035
+ const advanceWorker = () => {
25036
+ while (currentWorker && remainingLinesInWorker <= 0) {
25037
+ workerIndex++;
25038
+ currentWorker = executorResults[workerIndex] ?? null;
25039
+ remainingLinesInWorker = currentWorker?.outputCount ?? 0;
25040
+ }
25041
+ };
25042
+ advanceWorker();
25043
+ for await (const line of lineReader) {
25044
+ advanceWorker();
25045
+ Affirm_default(currentWorker, `Unable to resolve worker metadata while processing lineCount for ${datasetPath}`);
25046
+ const rowOffset = currentWorker.rowOffset ?? 0;
25047
+ if (rowOffset > 0) {
25048
+ const record = this._parseLine(line, internalRecordFormat, internalFields);
25049
+ for (const field of lineCountFields) {
25050
+ const currentValue = Number(record[field.finalKey]);
25051
+ Affirm_default(!Number.isNaN(currentValue), `Invalid lineCount value found while processing ${datasetPath}`);
25052
+ record[field.finalKey] = currentValue + rowOffset;
25053
+ }
25054
+ if (!writer.write(OutputExecutor_default.outputRecord(record, consumer, internalFields) + "\n"))
25055
+ await waitForDrain();
25056
+ } else if (!writer.write(line + "\n")) {
25057
+ await waitForDrain();
25058
+ }
25059
+ remainingLinesInWorker--;
25060
+ processedLineCount++;
25061
+ }
25062
+ lineReader.close();
25063
+ await new Promise((resolve, reject) => {
25064
+ writer.on("close", resolve);
25065
+ writer.on("error", reject);
25066
+ writer.end();
25067
+ });
25068
+ if (!reader.destroyed) {
25069
+ await new Promise((resolve) => {
25070
+ reader.once("close", resolve);
25071
+ reader.destroy();
25072
+ });
25073
+ }
25074
+ await import_promises8.default.unlink(datasetPath);
25075
+ await import_promises8.default.rename(tempWorkPath, datasetPath);
25076
+ return processedLineCount;
25077
+ };
24966
25078
  this.processDistinct = async (datasetPath) => {
24967
25079
  const reader = import_fs12.default.createReadStream(datasetPath);
24968
25080
  const lineReader = import_readline6.default.createInterface({ input: reader, crlfDelay: Infinity });
@@ -25554,6 +25666,7 @@ var ExecutorOrchestratorClass = class {
25554
25666
  const _progress = new ExecutorProgress2_default(logProgress);
25555
25667
  const { usageId } = UsageManager_default.startUsage(consumer, details);
25556
25668
  const scope = { id: usageId, folder: `${consumer.name}_${usageId}`, workersId: [], limitFileSize: consumer.maximumFileSize };
25669
+ const expandedFields = ConsumerManager_default.getExpandedFields(consumer);
25557
25670
  let activePool = null;
25558
25671
  try {
25559
25672
  const start = performance.now();
@@ -25616,7 +25729,9 @@ var ExecutorOrchestratorClass = class {
25616
25729
  }));
25617
25730
  }
25618
25731
  Logger_default.log(`[${usageId}] Waiting for ${workerThreads.length} worker(s) to complete | ${OrchestratorHelper_default.formatMemoryUsage()}`);
25619
- executorResults.push(...await Promise.all(workerThreads));
25732
+ const fileExecutorResults = await Promise.all(workerThreads);
25733
+ this.assignChunkRowMetadata(chunks, fileExecutorResults);
25734
+ executorResults.push(...fileExecutorResults);
25620
25735
  Logger_default.log(`[${usageId}] All ${workerThreads.length} worker(s) finished for producer "${prod.name}" file ${fileIndex + 1}/${totalFiles} | ${OrchestratorHelper_default.formatMemoryUsage()}`);
25621
25736
  } finally {
25622
25737
  await activePool.terminate();
@@ -25651,14 +25766,21 @@ var ExecutorOrchestratorClass = class {
25651
25766
  postOperation.totalOutputCount = unifiedOutputCount;
25652
25767
  Logger_default.log(`[${usageId}] DistinctOn pass complete: ${unifiedOutputCount} rows in ${Math.round(performance.now() - counter)}ms`);
25653
25768
  }
25654
- }
25655
- if (consumer.options?.pivot) {
25656
- Logger_default.log(`[${usageId}] Running pivot operation`);
25657
- counter = performance.now();
25658
- const unifiedOutputCount = await ConsumerExecutor_default.processPivot(consumer, ExecutorScope_default.getMainPath(scope));
25659
- tracker.measure("process-pivot:main", performance.now() - counter);
25660
- postOperation.totalOutputCount = unifiedOutputCount;
25661
- Logger_default.log(`[${usageId}] Pivot complete: ${unifiedOutputCount} rows in ${Math.round(performance.now() - counter)}ms`);
25769
+ if (consumer.options?.pivot) {
25770
+ Logger_default.log(`[${usageId}] Running pivot operation`);
25771
+ counter = performance.now();
25772
+ const unifiedOutputCount = await ConsumerExecutor_default.processPivot(consumer, ExecutorScope_default.getMainPath(scope));
25773
+ tracker.measure("process-pivot:main", performance.now() - counter);
25774
+ postOperation.totalOutputCount = unifiedOutputCount;
25775
+ Logger_default.log(`[${usageId}] Pivot complete: ${unifiedOutputCount} rows in ${Math.round(performance.now() - counter)}ms`);
25776
+ }
25777
+ if (ConsumerManager_default.hasLineCountFields(consumer, true)) {
25778
+ Logger_default.log(`[${usageId}] Running unified lineCount pass across merged workers`);
25779
+ counter = performance.now();
25780
+ const processedLineCount = await ConsumerExecutor_default.processLineCount(consumer, ExecutorScope_default.getMainPath(scope), executorResults);
25781
+ tracker.measure("process-line-count:main", performance.now() - counter);
25782
+ Logger_default.log(`[${usageId}] LineCount pass complete: normalized ${processedLineCount} rows in ${Math.round(performance.now() - counter)}ms`);
25783
+ }
25662
25784
  }
25663
25785
  if (consumer.validate && consumer.validate.length > 0) {
25664
25786
  Logger_default.log(`[${usageId}] Running dataset-level validations`);
@@ -25688,7 +25810,7 @@ var ExecutorOrchestratorClass = class {
25688
25810
  _progress.update({ phase: "Exporting result", progress: 0 });
25689
25811
  counter = performance.now();
25690
25812
  Logger_default.log(`[${usageId}] Exporting results to ${consumer.outputs.length} output(s)`);
25691
- const exportRes = await OutputExecutor_default.exportResult(consumer, ConsumerManager_default.getExpandedFields(consumer), scope);
25813
+ const exportRes = await OutputExecutor_default.exportResult(consumer, expandedFields, scope);
25692
25814
  tracker.measure("export-result", performance.now() - counter);
25693
25815
  Logger_default.log(`[${usageId}] Export complete in ${Math.round(performance.now() - counter)}ms (key: ${exportRes.key})`);
25694
25816
  _progress.update({ phase: "Exporting result", progress: 1 });
@@ -25759,6 +25881,22 @@ var ExecutorOrchestratorClass = class {
25759
25881
  import_fs13.default.closeSync(fd);
25760
25882
  }
25761
25883
  };
25884
+ this.assignChunkRowMetadata = (chunks, executorResults) => {
25885
+ const validResults = executorResults.filter((result) => Algo_default.hasVal(result));
25886
+ const resultsByChunkIndex = new Map(validResults.map((result) => [result.chunkIndex, result]));
25887
+ let nextRowOffset = 0;
25888
+ for (const chunk of chunks) {
25889
+ const result = resultsByChunkIndex.get(chunk.index);
25890
+ if (!result)
25891
+ continue;
25892
+ const rowCount = result.inputCount;
25893
+ chunk.rowOffset = nextRowOffset;
25894
+ chunk.rowCount = rowCount;
25895
+ result.rowOffset = nextRowOffset;
25896
+ result.rowCount = rowCount;
25897
+ nextRowOffset += rowCount;
25898
+ }
25899
+ };
25762
25900
  /**
25763
25901
  * Efficiently finds the next newline character starting from a position.
25764
25902
  * Uses small buffer reads for speed.
@@ -206,6 +206,10 @@
206
206
  "type": "boolean",
207
207
  "description": "If set, \"default\" must have a value. This field is not searched in the underlying dataset, but is a fixed value set by the \"default\" prop."
208
208
  },
209
+ "lineCount": {
210
+ "type": "boolean",
211
+ "description": "If set, this field is populated with the 1-based source line number of the current record. For multi-worker file processing the final value is normalized during merge so it remains consistent across chunks."
212
+ },
209
213
  "copyFrom": {
210
214
  "type": "string",
211
215
  "description": "If set, this field will be added as new to the consumer dataset and will be a copy of the specified field. Use the alias if set, otherwise the key. The source field should come before this field in the fields list."
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@forzalabs/remora",
3
- "version": "1.2.10",
3
+ "version": "1.2.11",
4
4
  "description": "A powerful CLI tool for seamless data translation.",
5
5
  "main": "index.js",
6
6
  "private": false,
@@ -18745,7 +18745,7 @@ var import_promises = __toESM(require("fs/promises"), 1);
18745
18745
 
18746
18746
  // ../../packages/constants/src/Constants.ts
18747
18747
  var CONSTANTS = {
18748
- cliVersion: "1.2.10",
18748
+ cliVersion: "1.2.11",
18749
18749
  backendVersion: 1,
18750
18750
  backendPort: 5088,
18751
18751
  workerVersion: 2,
@@ -19184,7 +19184,16 @@ var ValidatorClass = class {
19184
19184
  }
19185
19185
  const allAvailableFields = [...availableFieldsByProducer.values()].flat();
19186
19186
  for (const field of consumer.fields) {
19187
- if (field.key === "*" || field.fixed || field.copyFrom) continue;
19187
+ const syntheticFieldModes = [field.fixed === true, !!field.copyFrom, field.lineCount === true].filter(Boolean);
19188
+ if (syntheticFieldModes.length > 1)
19189
+ errors.push(`Field "${field.alias ?? field.key}" in consumer "${consumer.name}" can only use one synthetic field mode among "fixed", "copyFrom", and "lineCount".`);
19190
+ if (field.lineCount && field.key === "*")
19191
+ errors.push(`Field "${field.alias ?? field.key}" in consumer "${consumer.name}" cannot use "lineCount" with the wildcard key "*".`);
19192
+ if (field.lineCount && field.transform)
19193
+ errors.push(`Field "${field.alias ?? field.key}" in consumer "${consumer.name}" cannot use "transform" together with "lineCount".`);
19194
+ if (field.lineCount && field.validate && field.validate.length > 0)
19195
+ errors.push(`Field "${field.alias ?? field.key}" in consumer "${consumer.name}" cannot use "validate" together with "lineCount".`);
19196
+ if (field.key === "*" || field.fixed || field.copyFrom || field.lineCount) continue;
19188
19197
  if (field.from) {
19189
19198
  const producerFields = availableFieldsByProducer.get(field.from);
19190
19199
  if (producerFields && !producerFields.includes(field.key))
@@ -19205,6 +19214,8 @@ var ValidatorClass = class {
19205
19214
  const found = precedingFields.find((f) => (f.alias ?? f.key) === field.copyFrom);
19206
19215
  if (!found)
19207
19216
  errors.push(`Field "${field.alias ?? field.key}" uses copyFrom "${field.copyFrom}" but no field with that name/alias exists before it in consumer "${consumer.name}".`);
19217
+ else if (found.lineCount)
19218
+ errors.push(`Field "${field.alias ?? field.key}" in consumer "${consumer.name}" cannot use copyFrom on a field with "lineCount" because line counts are normalized after worker merge.`);
19208
19219
  }
19209
19220
  if (consumer.filters && consumer.filters.length > 0) {
19210
19221
  if (consumer.filters.some((x) => x.sql && x.rule))
@@ -19225,10 +19236,14 @@ var ValidatorClass = class {
19225
19236
  const missingRules = rulesWithMatchingFields.filter((x) => !x.match);
19226
19237
  errors.push(`Filter(s) on member(s) "${missingRules.map((x) => x.rule.rule.member).join(", ")}" is invalid since the member specified is not present in the consumer. Check the member value or add the missing field to the consumer.`);
19227
19238
  }
19239
+ const lineCountRuleMembers = rulesWithMatchingFields.filter((x) => x.match?.lineCount);
19240
+ if (lineCountRuleMembers.length > 0)
19241
+ errors.push(`Consumer "${consumer.name}" cannot use rule-based filters on field(s) with "lineCount": ${lineCountRuleMembers.map((x) => `"${x.rule.rule.member}"`).join(", ")}.`);
19228
19242
  }
19229
19243
  const validateTransformations = (fields) => {
19230
19244
  const errors2 = [];
19231
19245
  const trxsFields = fields.filter((x) => x.transform);
19246
+ const lineCountFieldNames = new Set(fields.filter((x) => x.lineCount).map((x) => x.alias ?? x.key));
19232
19247
  for (const field of trxsFields) {
19233
19248
  const trxToValidate = [];
19234
19249
  if (Array.isArray(field.transform))
@@ -19236,6 +19251,16 @@ var ValidatorClass = class {
19236
19251
  else
19237
19252
  trxToValidate.push(field.transform);
19238
19253
  for (const trans of trxToValidate) {
19254
+ if ("multiplyBy" in trans) {
19255
+ const invalidFields = trans.multiplyBy.fields.filter((x) => lineCountFieldNames.has(x));
19256
+ if (invalidFields.length > 0)
19257
+ errors2.push(`Transformation on field "${field.alias ?? field.key}" cannot reference lineCount field(s) in multiplyBy: ${invalidFields.map((x) => `"${x}"`).join(", ")}.`);
19258
+ }
19259
+ if ("addBy" in trans) {
19260
+ const invalidFields = trans.addBy.fields.filter((x) => lineCountFieldNames.has(x));
19261
+ if (invalidFields.length > 0)
19262
+ errors2.push(`Transformation on field "${field.alias ?? field.key}" cannot reference lineCount field(s) in addBy: ${invalidFields.map((x) => `"${x}"`).join(", ")}.`);
19263
+ }
19239
19264
  if ("combine_fields" in trans) {
19240
19265
  const { combine_fields } = trans;
19241
19266
  if (!combine_fields.fields || combine_fields.fields.length === 0)
@@ -19243,6 +19268,9 @@ var ValidatorClass = class {
19243
19268
  const missingFieldsInConsumer = combine_fields.fields.map((x) => ({ field: x, found: fields.find((k) => (k.alias ?? k.key) === x) })).filter((x) => !x.found);
19244
19269
  if (missingFieldsInConsumer.length > 0)
19245
19270
  errors2.push(`The requested field(s) for a transformation is missing in the consumer -> missing field(s): "${missingFieldsInConsumer.map((x) => x.field).join(", ")}"; field transformation: "${field.alias ?? field.key}";`);
19271
+ const invalidFields = combine_fields.fields.filter((x) => lineCountFieldNames.has(x));
19272
+ if (invalidFields.length > 0)
19273
+ errors2.push(`Transformation on field "${field.alias ?? field.key}" cannot reference lineCount field(s) in combine_fields: ${invalidFields.map((x) => `"${x}"`).join(", ")}.`);
19246
19274
  }
19247
19275
  }
19248
19276
  }
@@ -19265,7 +19293,7 @@ var ValidatorClass = class {
19265
19293
  errors.push(`The export destination "${output.exportDestination}" was not found in the sources.`);
19266
19294
  }
19267
19295
  }
19268
- const dimensionFields = consumer.fields.filter((x) => x.key !== "*" && !x.fixed && !x.copyFrom);
19296
+ const dimensionFields = consumer.fields.filter((x) => x.key !== "*" && !x.fixed && !x.copyFrom && !x.lineCount);
19269
19297
  const dimensionKeys = dimensionFields.map((x) => `${x.from ?? "_default_"}::${x.key}`);
19270
19298
  const duplicateDimensionKeys = dimensionKeys.filter((k, i) => dimensionKeys.indexOf(k) !== i);
19271
19299
  if (duplicateDimensionKeys.length > 0) {
@@ -22783,6 +22811,25 @@ var ConsumerManagerClass = class {
22783
22811
  const expandedFields = convertedFields.flatMap((x) => this.expandField(consumer, x, availableColumns));
22784
22812
  return expandedFields;
22785
22813
  };
22814
+ this.getLineCountFields = (consumer, visibleOnly = false) => {
22815
+ Affirm_default(consumer, "Invalid consumer");
22816
+ return this.getLineCountFieldsFromExpanded(this.getExpandedFields(consumer), visibleOnly);
22817
+ };
22818
+ this.getLineCountFieldsFromExpanded = (fields, visibleOnly = false) => {
22819
+ Affirm_default(fields, "Invalid consumer fields");
22820
+ return fields.filter((field) => this.isLineCountField(field.cField) && (!visibleOnly || !field.cField.hidden));
22821
+ };
22822
+ this.hasLineCountFields = (consumer, visibleOnly = false) => {
22823
+ return this.getLineCountFields(consumer, visibleOnly).length > 0;
22824
+ };
22825
+ this.isSyntheticField = (field) => {
22826
+ Affirm_default(field, "Invalid field");
22827
+ return field.fixed === true || !!field.copyFrom || this.isLineCountField(field);
22828
+ };
22829
+ this.isLineCountField = (field) => {
22830
+ Affirm_default(field, "Invalid field");
22831
+ return field.lineCount === true;
22832
+ };
22786
22833
  this.getAvailableColumns = (consumer) => {
22787
22834
  return ResourcesUtils_default.getAvailableColumns(consumer);
22788
22835
  };
@@ -22835,7 +22882,7 @@ var ConsumerManagerClass = class {
22835
22882
  column = columns.find((x) => x.owner === field.from && x.nameInProducer === field.key);
22836
22883
  } else if (consumer.producers.length === 1 && !field.from) {
22837
22884
  column = columns.find((x) => x.nameInProducer === field.key);
22838
- } else if (!field.fixed && !field.copyFrom) {
22885
+ } else if (!this.isSyntheticField(field)) {
22839
22886
  const matches = columns.filter((x) => x.nameInProducer === field.key);
22840
22887
  Affirm_default(matches.length > 0, `Consumer "${consumer.name}" misconfiguration: the field "${field.key}" is not found in any of the included producers (${consumer.producers.map((x) => x.name).join(", ")})`);
22841
22888
  if (matches.length === 1) {
@@ -22846,14 +22893,14 @@ var ConsumerManagerClass = class {
22846
22893
  column = matches[0];
22847
22894
  }
22848
22895
  if (!column) {
22849
- if (field.fixed === true && Algo_default.hasVal(field.default) || field.copyFrom) {
22896
+ if (field.fixed === true && Algo_default.hasVal(field.default) || this.isSyntheticField(field)) {
22850
22897
  column = {
22851
22898
  aliasInProducer: field.key,
22852
22899
  nameInProducer: field.alias ?? field.key,
22853
22900
  consumerAlias: field.alias ?? field.key,
22854
22901
  consumerKey: field.key,
22855
22902
  owner: field.from,
22856
- dimension: { name: field.key, type: typeof field.default }
22903
+ dimension: { name: field.key, type: this.isLineCountField(field) ? "number" : typeof field.default }
22857
22904
  };
22858
22905
  }
22859
22906
  }
@@ -24434,16 +24481,23 @@ var ConsumerExecutorClass = class {
24434
24481
  for (const field of fields) {
24435
24482
  const { cField } = field;
24436
24483
  const fieldKey = cField.alias ?? cField.key;
24484
+ if (cField.fixed && Algo_default.hasVal(cField.default)) {
24485
+ record[fieldKey] = cField.default;
24486
+ continue;
24487
+ }
24488
+ if (ConsumerManager_default.isLineCountField(cField)) {
24489
+ record[fieldKey] = recordIndex + 1;
24490
+ continue;
24491
+ }
24492
+ if (cField.copyFrom) {
24493
+ record[fieldKey] = record[cField.copyFrom];
24494
+ continue;
24495
+ }
24437
24496
  let dimension;
24438
24497
  try {
24439
24498
  dimension = dimensions.find((x) => x.name === cField.key);
24440
24499
  if (!dimension) {
24441
- if (cField.fixed && Algo_default.hasVal(cField.default))
24442
- record[fieldKey] = cField.default;
24443
- else if (cField.copyFrom)
24444
- record[fieldKey] = record[cField.copyFrom];
24445
- else
24446
- throw new Error(`The requested field "${cField.key}" from the consumer is not present in the underlying producer "${producer.name}" (${dimensions.map((x) => x.name).join(", ")})`);
24500
+ throw new Error(`The requested field "${cField.key}" from the consumer is not present in the underlying producer "${producer.name}" (${dimensions.map((x) => x.name).join(", ")})`);
24447
24501
  }
24448
24502
  } catch (error) {
24449
24503
  const err2 = new Error(`Resolving dimension for field "${fieldKey}" of producer "${producer.name}" failed (index: ${recordIndex}): ${error.message}`, { cause: error });
@@ -24556,6 +24610,64 @@ var ConsumerExecutorClass = class {
24556
24610
  }
24557
24611
  return record;
24558
24612
  };
24613
+ this.processLineCount = async (consumer, datasetPath, executorResults) => {
24614
+ const internalRecordFormat = OutputExecutor_default._getInternalRecordFormat(consumer);
24615
+ const internalFields = ConsumerManager_default.getExpandedFields(consumer);
24616
+ const lineCountFields = ConsumerManager_default.getLineCountFieldsFromExpanded(internalFields, true);
24617
+ if (lineCountFields.length === 0)
24618
+ return 0;
24619
+ const tempWorkPath = datasetPath + "_tmp";
24620
+ const reader = import_fs10.default.createReadStream(datasetPath);
24621
+ const lineReader = import_readline6.default.createInterface({ input: reader, crlfDelay: Infinity });
24622
+ const writer = import_fs10.default.createWriteStream(tempWorkPath);
24623
+ const waitForDrain = () => new Promise((resolve) => writer.once("drain", resolve));
24624
+ let workerIndex = 0;
24625
+ let currentWorker = executorResults[workerIndex] ?? null;
24626
+ let remainingLinesInWorker = currentWorker?.outputCount ?? 0;
24627
+ let processedLineCount = 0;
24628
+ const advanceWorker = () => {
24629
+ while (currentWorker && remainingLinesInWorker <= 0) {
24630
+ workerIndex++;
24631
+ currentWorker = executorResults[workerIndex] ?? null;
24632
+ remainingLinesInWorker = currentWorker?.outputCount ?? 0;
24633
+ }
24634
+ };
24635
+ advanceWorker();
24636
+ for await (const line of lineReader) {
24637
+ advanceWorker();
24638
+ Affirm_default(currentWorker, `Unable to resolve worker metadata while processing lineCount for ${datasetPath}`);
24639
+ const rowOffset = currentWorker.rowOffset ?? 0;
24640
+ if (rowOffset > 0) {
24641
+ const record = this._parseLine(line, internalRecordFormat, internalFields);
24642
+ for (const field of lineCountFields) {
24643
+ const currentValue = Number(record[field.finalKey]);
24644
+ Affirm_default(!Number.isNaN(currentValue), `Invalid lineCount value found while processing ${datasetPath}`);
24645
+ record[field.finalKey] = currentValue + rowOffset;
24646
+ }
24647
+ if (!writer.write(OutputExecutor_default.outputRecord(record, consumer, internalFields) + "\n"))
24648
+ await waitForDrain();
24649
+ } else if (!writer.write(line + "\n")) {
24650
+ await waitForDrain();
24651
+ }
24652
+ remainingLinesInWorker--;
24653
+ processedLineCount++;
24654
+ }
24655
+ lineReader.close();
24656
+ await new Promise((resolve, reject) => {
24657
+ writer.on("close", resolve);
24658
+ writer.on("error", reject);
24659
+ writer.end();
24660
+ });
24661
+ if (!reader.destroyed) {
24662
+ await new Promise((resolve) => {
24663
+ reader.once("close", resolve);
24664
+ reader.destroy();
24665
+ });
24666
+ }
24667
+ await import_promises8.default.unlink(datasetPath);
24668
+ await import_promises8.default.rename(tempWorkPath, datasetPath);
24669
+ return processedLineCount;
24670
+ };
24559
24671
  this.processDistinct = async (datasetPath) => {
24560
24672
  const reader = import_fs10.default.createReadStream(datasetPath);
24561
24673
  const lineReader = import_readline6.default.createInterface({ input: reader, crlfDelay: Infinity });
@@ -24912,6 +25024,10 @@ var Executor = class {
24912
25024
  elapsedMS: -1,
24913
25025
  inputCount: -1,
24914
25026
  outputCount: -1,
25027
+ chunkIndex: chunk.index,
25028
+ fileUri: chunk.fileUri,
25029
+ rowOffset: chunk.rowOffset ?? -1,
25030
+ rowCount: -1,
24915
25031
  resultUri: ExecutorScope_default.getWorkerPath(scope, workerId),
24916
25032
  operations: {}
24917
25033
  };
@@ -25021,6 +25137,7 @@ var Executor = class {
25021
25137
  result.cycles = totalCycles;
25022
25138
  result.inputCount = lineIndex;
25023
25139
  result.outputCount = totalOutputCount;
25140
+ result.rowCount = lineIndex;
25024
25141
  result.operations = this._performance.getOperations();
25025
25142
  Logger_default.log(`[${workerId}] Finished: ${lineIndex} input \u2192 ${totalOutputCount} output in ${Math.round(result.elapsedMS)}ms (${totalCycles} cycle(s))`);
25026
25143
  return result;
@@ -25321,6 +25438,7 @@ var ExecutorOrchestratorClass = class {
25321
25438
  const _progress = new ExecutorProgress2_default(logProgress);
25322
25439
  const { usageId } = UsageManager_default.startUsage(consumer, details);
25323
25440
  const scope = { id: usageId, folder: `${consumer.name}_${usageId}`, workersId: [], limitFileSize: consumer.maximumFileSize };
25441
+ const expandedFields = ConsumerManager_default.getExpandedFields(consumer);
25324
25442
  let activePool = null;
25325
25443
  try {
25326
25444
  const start = performance.now();
@@ -25383,7 +25501,9 @@ var ExecutorOrchestratorClass = class {
25383
25501
  }));
25384
25502
  }
25385
25503
  Logger_default.log(`[${usageId}] Waiting for ${workerThreads.length} worker(s) to complete | ${OrchestratorHelper_default.formatMemoryUsage()}`);
25386
- executorResults.push(...await Promise.all(workerThreads));
25504
+ const fileExecutorResults = await Promise.all(workerThreads);
25505
+ this.assignChunkRowMetadata(chunks, fileExecutorResults);
25506
+ executorResults.push(...fileExecutorResults);
25387
25507
  Logger_default.log(`[${usageId}] All ${workerThreads.length} worker(s) finished for producer "${prod.name}" file ${fileIndex + 1}/${totalFiles} | ${OrchestratorHelper_default.formatMemoryUsage()}`);
25388
25508
  } finally {
25389
25509
  await activePool.terminate();
@@ -25418,14 +25538,21 @@ var ExecutorOrchestratorClass = class {
25418
25538
  postOperation.totalOutputCount = unifiedOutputCount;
25419
25539
  Logger_default.log(`[${usageId}] DistinctOn pass complete: ${unifiedOutputCount} rows in ${Math.round(performance.now() - counter)}ms`);
25420
25540
  }
25421
- }
25422
- if (consumer.options?.pivot) {
25423
- Logger_default.log(`[${usageId}] Running pivot operation`);
25424
- counter = performance.now();
25425
- const unifiedOutputCount = await ConsumerExecutor_default.processPivot(consumer, ExecutorScope_default.getMainPath(scope));
25426
- tracker.measure("process-pivot:main", performance.now() - counter);
25427
- postOperation.totalOutputCount = unifiedOutputCount;
25428
- Logger_default.log(`[${usageId}] Pivot complete: ${unifiedOutputCount} rows in ${Math.round(performance.now() - counter)}ms`);
25541
+ if (consumer.options?.pivot) {
25542
+ Logger_default.log(`[${usageId}] Running pivot operation`);
25543
+ counter = performance.now();
25544
+ const unifiedOutputCount = await ConsumerExecutor_default.processPivot(consumer, ExecutorScope_default.getMainPath(scope));
25545
+ tracker.measure("process-pivot:main", performance.now() - counter);
25546
+ postOperation.totalOutputCount = unifiedOutputCount;
25547
+ Logger_default.log(`[${usageId}] Pivot complete: ${unifiedOutputCount} rows in ${Math.round(performance.now() - counter)}ms`);
25548
+ }
25549
+ if (ConsumerManager_default.hasLineCountFields(consumer, true)) {
25550
+ Logger_default.log(`[${usageId}] Running unified lineCount pass across merged workers`);
25551
+ counter = performance.now();
25552
+ const processedLineCount = await ConsumerExecutor_default.processLineCount(consumer, ExecutorScope_default.getMainPath(scope), executorResults);
25553
+ tracker.measure("process-line-count:main", performance.now() - counter);
25554
+ Logger_default.log(`[${usageId}] LineCount pass complete: normalized ${processedLineCount} rows in ${Math.round(performance.now() - counter)}ms`);
25555
+ }
25429
25556
  }
25430
25557
  if (consumer.validate && consumer.validate.length > 0) {
25431
25558
  Logger_default.log(`[${usageId}] Running dataset-level validations`);
@@ -25455,7 +25582,7 @@ var ExecutorOrchestratorClass = class {
25455
25582
  _progress.update({ phase: "Exporting result", progress: 0 });
25456
25583
  counter = performance.now();
25457
25584
  Logger_default.log(`[${usageId}] Exporting results to ${consumer.outputs.length} output(s)`);
25458
- const exportRes = await OutputExecutor_default.exportResult(consumer, ConsumerManager_default.getExpandedFields(consumer), scope);
25585
+ const exportRes = await OutputExecutor_default.exportResult(consumer, expandedFields, scope);
25459
25586
  tracker.measure("export-result", performance.now() - counter);
25460
25587
  Logger_default.log(`[${usageId}] Export complete in ${Math.round(performance.now() - counter)}ms (key: ${exportRes.key})`);
25461
25588
  _progress.update({ phase: "Exporting result", progress: 1 });
@@ -25526,6 +25653,22 @@ var ExecutorOrchestratorClass = class {
25526
25653
  import_fs12.default.closeSync(fd);
25527
25654
  }
25528
25655
  };
25656
+ this.assignChunkRowMetadata = (chunks, executorResults) => {
25657
+ const validResults = executorResults.filter((result) => Algo_default.hasVal(result));
25658
+ const resultsByChunkIndex = new Map(validResults.map((result) => [result.chunkIndex, result]));
25659
+ let nextRowOffset = 0;
25660
+ for (const chunk of chunks) {
25661
+ const result = resultsByChunkIndex.get(chunk.index);
25662
+ if (!result)
25663
+ continue;
25664
+ const rowCount = result.inputCount;
25665
+ chunk.rowOffset = nextRowOffset;
25666
+ chunk.rowCount = rowCount;
25667
+ result.rowOffset = nextRowOffset;
25668
+ result.rowCount = rowCount;
25669
+ nextRowOffset += rowCount;
25670
+ }
25671
+ };
25529
25672
  /**
25530
25673
  * Efficiently finds the next newline character starting from a position.
25531
25674
  * Uses small buffer reads for speed.