@forzalabs/remora 2.1.1 → 2.2.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.
@@ -72,6 +72,67 @@ var init_Affirm = __esm({
72
72
  }
73
73
  });
74
74
 
75
+ // ../../packages/core/src/BatchWriter.ts
76
+ var FLUSH_AFTER_CHARS, BatchWriter, BatchWriter_default;
77
+ var init_BatchWriter = __esm({
78
+ "../../packages/core/src/BatchWriter.ts"() {
79
+ init_Affirm();
80
+ FLUSH_AFTER_CHARS = 1 << 18;
81
+ BatchWriter = class {
82
+ constructor(stream) {
83
+ /** How many times the stream's buffer filled and the caller had to wait for it. */
84
+ this.drains = 0;
85
+ this.buffer = "";
86
+ /** Queues one line and its newline. False means a batch went out and the stream wants a drain. */
87
+ this.write = (line) => {
88
+ this.buffer += line;
89
+ this.buffer += "\n";
90
+ if (this.buffer.length < FLUSH_AFTER_CHARS)
91
+ return true;
92
+ return this.flush();
93
+ };
94
+ this.waitForDrain = () => {
95
+ this.drains++;
96
+ return new Promise((resolve) => this.stream.once("drain", resolve));
97
+ };
98
+ /**
99
+ * Writes whatever is still buffered, then waits for the stream to be fully closed.
100
+ *
101
+ * Waits for `close`, not `finish`: `finish` only means `end()` has flushed its buffer to the OS,
102
+ * while `close` means the descriptor is released. Callers that rename or unlink the file right
103
+ * after — which most of the passes that write a dataset do — need the descriptor gone, or the
104
+ * rename fails with EPERM on Windows. `close` is strictly later than `finish`, so waiting for it
105
+ * is safe for the callers that do not care.
106
+ *
107
+ * The flush above may hand the stream more than its high-water mark, which `write` would report
108
+ * by returning false. There is nothing to wait for here: `end()` flushes what is buffered before
109
+ * it emits `finish`, so the batch lands either way.
110
+ */
111
+ this.close = async () => {
112
+ if (this.stream.destroyed)
113
+ return;
114
+ this.flush();
115
+ await new Promise((resolve, reject) => {
116
+ this.stream.once("close", resolve);
117
+ this.stream.once("error", reject);
118
+ this.stream.end();
119
+ });
120
+ };
121
+ this.flush = () => {
122
+ if (!this.buffer)
123
+ return true;
124
+ const batch = this.buffer;
125
+ this.buffer = "";
126
+ return this.stream.write(batch);
127
+ };
128
+ Affirm_default(stream, "Invalid write stream");
129
+ this.stream = stream;
130
+ }
131
+ };
132
+ BatchWriter_default = BatchWriter;
133
+ }
134
+ });
135
+
75
136
  // ../../packages/core/src/Algo.ts
76
137
  var algo, Algo_default;
77
138
  var init_Algo = __esm({
@@ -233,6 +294,36 @@ var init_Algo = __esm({
233
294
  }
234
295
  });
235
296
 
297
+ // ../../packages/core/src/LineReader.ts
298
+ async function* readLines(input) {
299
+ Affirm_default(input, "Invalid input stream");
300
+ input.setEncoding("utf8");
301
+ let tail = "";
302
+ for await (const chunk of input) {
303
+ let text = tail + chunk;
304
+ let pendingReturn = "";
305
+ if (text.endsWith("\r")) {
306
+ pendingReturn = "\r";
307
+ text = text.slice(0, -1);
308
+ }
309
+ const parts = text.split(TERMINATOR);
310
+ tail = parts.pop() + pendingReturn;
311
+ for (const part of parts)
312
+ yield part;
313
+ }
314
+ if (tail.endsWith("\r"))
315
+ yield tail.slice(0, -1);
316
+ else if (tail.length > 0)
317
+ yield tail;
318
+ }
319
+ var TERMINATOR;
320
+ var init_LineReader = __esm({
321
+ "../../packages/core/src/LineReader.ts"() {
322
+ init_Affirm();
323
+ TERMINATOR = /\r\n|\n|\r/;
324
+ }
325
+ });
326
+
236
327
  // ../../packages/core/src/dste/DSTE.ts
237
328
  var DeterministicSimulationTestingEngine, DSTE, DSTE_default;
238
329
  var init_DSTE = __esm({
@@ -283,7 +374,9 @@ Tests summary:
283
374
  var init_src = __esm({
284
375
  "../../packages/core/src/index.ts"() {
285
376
  init_Affirm();
377
+ init_BatchWriter();
286
378
  init_Algo();
379
+ init_LineReader();
287
380
  init_DSTE();
288
381
  }
289
382
  });
@@ -4203,14 +4296,14 @@ var require_from = __commonJS({
4203
4296
  return (hint === "string" ? String : Number)(input);
4204
4297
  }
4205
4298
  var ERR_INVALID_ARG_TYPE = require_errors2().codes.ERR_INVALID_ARG_TYPE;
4206
- function from(Readable8, iterable, opts) {
4299
+ function from(Readable10, iterable, opts) {
4207
4300
  var iterator;
4208
4301
  if (iterable && typeof iterable.next === "function") {
4209
4302
  iterator = iterable;
4210
4303
  } else if (iterable && iterable[Symbol.asyncIterator]) iterator = iterable[Symbol.asyncIterator]();
4211
4304
  else if (iterable && iterable[Symbol.iterator]) iterator = iterable[Symbol.iterator]();
4212
4305
  else throw new ERR_INVALID_ARG_TYPE("iterable", ["Iterable"], iterable);
4213
- var readable = new Readable8(_objectSpread({
4306
+ var readable = new Readable10(_objectSpread({
4214
4307
  objectMode: true
4215
4308
  }, opts));
4216
4309
  var reading = false;
@@ -4250,9 +4343,9 @@ var require_from = __commonJS({
4250
4343
  var require_stream_readable = __commonJS({
4251
4344
  "../../node_modules/readable-stream/lib/_stream_readable.js"(exports2, module2) {
4252
4345
  "use strict";
4253
- module2.exports = Readable8;
4346
+ module2.exports = Readable10;
4254
4347
  var Duplex;
4255
- Readable8.ReadableState = ReadableState;
4348
+ Readable10.ReadableState = ReadableState;
4256
4349
  var EE = require("events").EventEmitter;
4257
4350
  var EElistenerCount = function EElistenerCount2(emitter, type) {
4258
4351
  return emitter.listeners(type).length;
@@ -4287,7 +4380,7 @@ var require_stream_readable = __commonJS({
4287
4380
  var StringDecoder;
4288
4381
  var createReadableStreamAsyncIterator;
4289
4382
  var from;
4290
- require_inherits()(Readable8, Stream);
4383
+ require_inherits()(Readable10, Stream);
4291
4384
  var errorOrDestroy = destroyImpl.errorOrDestroy;
4292
4385
  var kProxyEvents = ["error", "close", "destroy", "pause", "resume"];
4293
4386
  function prependListener(emitter, event, fn) {
@@ -4331,9 +4424,9 @@ var require_stream_readable = __commonJS({
4331
4424
  this.encoding = options2.encoding;
4332
4425
  }
4333
4426
  }
4334
- function Readable8(options2) {
4427
+ function Readable10(options2) {
4335
4428
  Duplex = Duplex || require_stream_duplex();
4336
- if (!(this instanceof Readable8)) return new Readable8(options2);
4429
+ if (!(this instanceof Readable10)) return new Readable10(options2);
4337
4430
  var isDuplex = this instanceof Duplex;
4338
4431
  this._readableState = new ReadableState(options2, this, isDuplex);
4339
4432
  this.readable = true;
@@ -4343,7 +4436,7 @@ var require_stream_readable = __commonJS({
4343
4436
  }
4344
4437
  Stream.call(this);
4345
4438
  }
4346
- Object.defineProperty(Readable8.prototype, "destroyed", {
4439
+ Object.defineProperty(Readable10.prototype, "destroyed", {
4347
4440
  // making it explicit this property is not enumerable
4348
4441
  // because otherwise some prototype manipulation in
4349
4442
  // userland will fail
@@ -4361,12 +4454,12 @@ var require_stream_readable = __commonJS({
4361
4454
  this._readableState.destroyed = value;
4362
4455
  }
4363
4456
  });
4364
- Readable8.prototype.destroy = destroyImpl.destroy;
4365
- Readable8.prototype._undestroy = destroyImpl.undestroy;
4366
- Readable8.prototype._destroy = function(err2, cb) {
4457
+ Readable10.prototype.destroy = destroyImpl.destroy;
4458
+ Readable10.prototype._undestroy = destroyImpl.undestroy;
4459
+ Readable10.prototype._destroy = function(err2, cb) {
4367
4460
  cb(err2);
4368
4461
  };
4369
- Readable8.prototype.push = function(chunk, encoding) {
4462
+ Readable10.prototype.push = function(chunk, encoding) {
4370
4463
  var state2 = this._readableState;
4371
4464
  var skipChunkCheck;
4372
4465
  if (!state2.objectMode) {
@@ -4383,7 +4476,7 @@ var require_stream_readable = __commonJS({
4383
4476
  }
4384
4477
  return readableAddChunk(this, chunk, encoding, false, skipChunkCheck);
4385
4478
  };
4386
- Readable8.prototype.unshift = function(chunk) {
4479
+ Readable10.prototype.unshift = function(chunk) {
4387
4480
  return readableAddChunk(this, chunk, null, true, false);
4388
4481
  };
4389
4482
  function readableAddChunk(stream, chunk, encoding, addToFront, skipChunkCheck) {
@@ -4444,10 +4537,10 @@ var require_stream_readable = __commonJS({
4444
4537
  }
4445
4538
  return er2;
4446
4539
  }
4447
- Readable8.prototype.isPaused = function() {
4540
+ Readable10.prototype.isPaused = function() {
4448
4541
  return this._readableState.flowing === false;
4449
4542
  };
4450
- Readable8.prototype.setEncoding = function(enc) {
4543
+ Readable10.prototype.setEncoding = function(enc) {
4451
4544
  if (!StringDecoder) StringDecoder = require_string_decoder().StringDecoder;
4452
4545
  var decoder = new StringDecoder(enc);
4453
4546
  this._readableState.decoder = decoder;
@@ -4493,7 +4586,7 @@ var require_stream_readable = __commonJS({
4493
4586
  }
4494
4587
  return state2.length;
4495
4588
  }
4496
- Readable8.prototype.read = function(n4) {
4589
+ Readable10.prototype.read = function(n4) {
4497
4590
  debug("read", n4);
4498
4591
  n4 = parseInt(n4, 10);
4499
4592
  var state2 = this._readableState;
@@ -4602,10 +4695,10 @@ var require_stream_readable = __commonJS({
4602
4695
  }
4603
4696
  state2.readingMore = false;
4604
4697
  }
4605
- Readable8.prototype._read = function(n4) {
4698
+ Readable10.prototype._read = function(n4) {
4606
4699
  errorOrDestroy(this, new ERR_METHOD_NOT_IMPLEMENTED("_read()"));
4607
4700
  };
4608
- Readable8.prototype.pipe = function(dest, pipeOpts) {
4701
+ Readable10.prototype.pipe = function(dest, pipeOpts) {
4609
4702
  var src = this;
4610
4703
  var state2 = this._readableState;
4611
4704
  switch (state2.pipesCount) {
@@ -4708,7 +4801,7 @@ var require_stream_readable = __commonJS({
4708
4801
  }
4709
4802
  };
4710
4803
  }
4711
- Readable8.prototype.unpipe = function(dest) {
4804
+ Readable10.prototype.unpipe = function(dest) {
4712
4805
  var state2 = this._readableState;
4713
4806
  var unpipeInfo = {
4714
4807
  hasUnpiped: false
@@ -4742,7 +4835,7 @@ var require_stream_readable = __commonJS({
4742
4835
  dest.emit("unpipe", this, unpipeInfo);
4743
4836
  return this;
4744
4837
  };
4745
- Readable8.prototype.on = function(ev, fn) {
4838
+ Readable10.prototype.on = function(ev, fn) {
4746
4839
  var res = Stream.prototype.on.call(this, ev, fn);
4747
4840
  var state2 = this._readableState;
4748
4841
  if (ev === "data") {
@@ -4763,15 +4856,15 @@ var require_stream_readable = __commonJS({
4763
4856
  }
4764
4857
  return res;
4765
4858
  };
4766
- Readable8.prototype.addListener = Readable8.prototype.on;
4767
- Readable8.prototype.removeListener = function(ev, fn) {
4859
+ Readable10.prototype.addListener = Readable10.prototype.on;
4860
+ Readable10.prototype.removeListener = function(ev, fn) {
4768
4861
  var res = Stream.prototype.removeListener.call(this, ev, fn);
4769
4862
  if (ev === "readable") {
4770
4863
  process.nextTick(updateReadableListening, this);
4771
4864
  }
4772
4865
  return res;
4773
4866
  };
4774
- Readable8.prototype.removeAllListeners = function(ev) {
4867
+ Readable10.prototype.removeAllListeners = function(ev) {
4775
4868
  var res = Stream.prototype.removeAllListeners.apply(this, arguments);
4776
4869
  if (ev === "readable" || ev === void 0) {
4777
4870
  process.nextTick(updateReadableListening, this);
@@ -4791,7 +4884,7 @@ var require_stream_readable = __commonJS({
4791
4884
  debug("readable nexttick read 0");
4792
4885
  self2.read(0);
4793
4886
  }
4794
- Readable8.prototype.resume = function() {
4887
+ Readable10.prototype.resume = function() {
4795
4888
  var state2 = this._readableState;
4796
4889
  if (!state2.flowing) {
4797
4890
  debug("resume");
@@ -4817,7 +4910,7 @@ var require_stream_readable = __commonJS({
4817
4910
  flow(stream);
4818
4911
  if (state2.flowing && !state2.reading) stream.read(0);
4819
4912
  }
4820
- Readable8.prototype.pause = function() {
4913
+ Readable10.prototype.pause = function() {
4821
4914
  debug("call pause flowing=%j", this._readableState.flowing);
4822
4915
  if (this._readableState.flowing !== false) {
4823
4916
  debug("pause");
@@ -4832,7 +4925,7 @@ var require_stream_readable = __commonJS({
4832
4925
  debug("flow", state2.flowing);
4833
4926
  while (state2.flowing && stream.read() !== null) ;
4834
4927
  }
4835
- Readable8.prototype.wrap = function(stream) {
4928
+ Readable10.prototype.wrap = function(stream) {
4836
4929
  var _this = this;
4837
4930
  var state2 = this._readableState;
4838
4931
  var paused = false;
@@ -4877,14 +4970,14 @@ var require_stream_readable = __commonJS({
4877
4970
  return this;
4878
4971
  };
4879
4972
  if (typeof Symbol === "function") {
4880
- Readable8.prototype[Symbol.asyncIterator] = function() {
4973
+ Readable10.prototype[Symbol.asyncIterator] = function() {
4881
4974
  if (createReadableStreamAsyncIterator === void 0) {
4882
4975
  createReadableStreamAsyncIterator = require_async_iterator();
4883
4976
  }
4884
4977
  return createReadableStreamAsyncIterator(this);
4885
4978
  };
4886
4979
  }
4887
- Object.defineProperty(Readable8.prototype, "readableHighWaterMark", {
4980
+ Object.defineProperty(Readable10.prototype, "readableHighWaterMark", {
4888
4981
  // making it explicit this property is not enumerable
4889
4982
  // because otherwise some prototype manipulation in
4890
4983
  // userland will fail
@@ -4893,7 +4986,7 @@ var require_stream_readable = __commonJS({
4893
4986
  return this._readableState.highWaterMark;
4894
4987
  }
4895
4988
  });
4896
- Object.defineProperty(Readable8.prototype, "readableBuffer", {
4989
+ Object.defineProperty(Readable10.prototype, "readableBuffer", {
4897
4990
  // making it explicit this property is not enumerable
4898
4991
  // because otherwise some prototype manipulation in
4899
4992
  // userland will fail
@@ -4902,7 +4995,7 @@ var require_stream_readable = __commonJS({
4902
4995
  return this._readableState && this._readableState.buffer;
4903
4996
  }
4904
4997
  });
4905
- Object.defineProperty(Readable8.prototype, "readableFlowing", {
4998
+ Object.defineProperty(Readable10.prototype, "readableFlowing", {
4906
4999
  // making it explicit this property is not enumerable
4907
5000
  // because otherwise some prototype manipulation in
4908
5001
  // userland will fail
@@ -4916,8 +5009,8 @@ var require_stream_readable = __commonJS({
4916
5009
  }
4917
5010
  }
4918
5011
  });
4919
- Readable8._fromList = fromList;
4920
- Object.defineProperty(Readable8.prototype, "readableLength", {
5012
+ Readable10._fromList = fromList;
5013
+ Object.defineProperty(Readable10.prototype, "readableLength", {
4921
5014
  // making it explicit this property is not enumerable
4922
5015
  // because otherwise some prototype manipulation in
4923
5016
  // userland will fail
@@ -4963,11 +5056,11 @@ var require_stream_readable = __commonJS({
4963
5056
  }
4964
5057
  }
4965
5058
  if (typeof Symbol === "function") {
4966
- Readable8.from = function(iterable, opts) {
5059
+ Readable10.from = function(iterable, opts) {
4967
5060
  if (from === void 0) {
4968
5061
  from = require_from();
4969
5062
  }
4970
- return from(Readable8, iterable, opts);
5063
+ return from(Readable10, iterable, opts);
4971
5064
  };
4972
5065
  }
4973
5066
  function indexOf(xs2, x2) {
@@ -4989,9 +5082,9 @@ var require_stream_duplex = __commonJS({
4989
5082
  return keys2;
4990
5083
  };
4991
5084
  module2.exports = Duplex;
4992
- var Readable8 = require_stream_readable();
5085
+ var Readable10 = require_stream_readable();
4993
5086
  var Writable2 = require_stream_writable();
4994
- require_inherits()(Duplex, Readable8);
5087
+ require_inherits()(Duplex, Readable10);
4995
5088
  {
4996
5089
  keys = objectKeys(Writable2.prototype);
4997
5090
  for (v3 = 0; v3 < keys.length; v3++) {
@@ -5004,7 +5097,7 @@ var require_stream_duplex = __commonJS({
5004
5097
  var v3;
5005
5098
  function Duplex(options2) {
5006
5099
  if (!(this instanceof Duplex)) return new Duplex(options2);
5007
- Readable8.call(this, options2);
5100
+ Readable10.call(this, options2);
5008
5101
  Writable2.call(this, options2);
5009
5102
  this.allowHalfOpen = true;
5010
5103
  if (options2) {
@@ -6374,14 +6467,14 @@ var require_series = __commonJS({
6374
6467
  var require_stream_transform = __commonJS({
6375
6468
  "../../node_modules/readable-stream/lib/_stream_transform.js"(exports2, module2) {
6376
6469
  "use strict";
6377
- module2.exports = Transform4;
6470
+ module2.exports = Transform5;
6378
6471
  var _require$codes = require_errors2().codes;
6379
6472
  var ERR_METHOD_NOT_IMPLEMENTED = _require$codes.ERR_METHOD_NOT_IMPLEMENTED;
6380
6473
  var ERR_MULTIPLE_CALLBACK = _require$codes.ERR_MULTIPLE_CALLBACK;
6381
6474
  var ERR_TRANSFORM_ALREADY_TRANSFORMING = _require$codes.ERR_TRANSFORM_ALREADY_TRANSFORMING;
6382
6475
  var ERR_TRANSFORM_WITH_LENGTH_0 = _require$codes.ERR_TRANSFORM_WITH_LENGTH_0;
6383
6476
  var Duplex = require_stream_duplex();
6384
- require_inherits()(Transform4, Duplex);
6477
+ require_inherits()(Transform5, Duplex);
6385
6478
  function afterTransform(er2, data) {
6386
6479
  var ts2 = this._transformState;
6387
6480
  ts2.transforming = false;
@@ -6400,8 +6493,8 @@ var require_stream_transform = __commonJS({
6400
6493
  this._read(rs2.highWaterMark);
6401
6494
  }
6402
6495
  }
6403
- function Transform4(options2) {
6404
- if (!(this instanceof Transform4)) return new Transform4(options2);
6496
+ function Transform5(options2) {
6497
+ if (!(this instanceof Transform5)) return new Transform5(options2);
6405
6498
  Duplex.call(this, options2);
6406
6499
  this._transformState = {
6407
6500
  afterTransform: afterTransform.bind(this),
@@ -6429,14 +6522,14 @@ var require_stream_transform = __commonJS({
6429
6522
  done(this, null, null);
6430
6523
  }
6431
6524
  }
6432
- Transform4.prototype.push = function(chunk, encoding) {
6525
+ Transform5.prototype.push = function(chunk, encoding) {
6433
6526
  this._transformState.needTransform = false;
6434
6527
  return Duplex.prototype.push.call(this, chunk, encoding);
6435
6528
  };
6436
- Transform4.prototype._transform = function(chunk, encoding, cb) {
6529
+ Transform5.prototype._transform = function(chunk, encoding, cb) {
6437
6530
  cb(new ERR_METHOD_NOT_IMPLEMENTED("_transform()"));
6438
6531
  };
6439
- Transform4.prototype._write = function(chunk, encoding, cb) {
6532
+ Transform5.prototype._write = function(chunk, encoding, cb) {
6440
6533
  var ts2 = this._transformState;
6441
6534
  ts2.writecb = cb;
6442
6535
  ts2.writechunk = chunk;
@@ -6446,7 +6539,7 @@ var require_stream_transform = __commonJS({
6446
6539
  if (ts2.needTransform || rs2.needReadable || rs2.length < rs2.highWaterMark) this._read(rs2.highWaterMark);
6447
6540
  }
6448
6541
  };
6449
- Transform4.prototype._read = function(n4) {
6542
+ Transform5.prototype._read = function(n4) {
6450
6543
  var ts2 = this._transformState;
6451
6544
  if (ts2.writechunk !== null && !ts2.transforming) {
6452
6545
  ts2.transforming = true;
@@ -6455,7 +6548,7 @@ var require_stream_transform = __commonJS({
6455
6548
  ts2.needTransform = true;
6456
6549
  }
6457
6550
  };
6458
- Transform4.prototype._destroy = function(err2, cb) {
6551
+ Transform5.prototype._destroy = function(err2, cb) {
6459
6552
  Duplex.prototype._destroy.call(this, err2, function(err22) {
6460
6553
  cb(err22);
6461
6554
  });
@@ -6476,11 +6569,11 @@ var require_stream_passthrough = __commonJS({
6476
6569
  "../../node_modules/readable-stream/lib/_stream_passthrough.js"(exports2, module2) {
6477
6570
  "use strict";
6478
6571
  module2.exports = PassThrough;
6479
- var Transform4 = require_stream_transform();
6480
- require_inherits()(PassThrough, Transform4);
6572
+ var Transform5 = require_stream_transform();
6573
+ require_inherits()(PassThrough, Transform5);
6481
6574
  function PassThrough(options2) {
6482
6575
  if (!(this instanceof PassThrough)) return new PassThrough(options2);
6483
- Transform4.call(this, options2);
6576
+ Transform5.call(this, options2);
6484
6577
  }
6485
6578
  PassThrough.prototype._transform = function(chunk, encoding, cb) {
6486
6579
  cb(null, chunk);
@@ -6546,7 +6639,7 @@ var require_pipeline = __commonJS({
6546
6639
  if (typeof streams[streams.length - 1] !== "function") return noop;
6547
6640
  return streams.pop();
6548
6641
  }
6549
- function pipeline6() {
6642
+ function pipeline7() {
6550
6643
  for (var _len = arguments.length, streams = new Array(_len), _key = 0; _key < _len; _key++) {
6551
6644
  streams[_key] = arguments[_key];
6552
6645
  }
@@ -6569,7 +6662,7 @@ var require_pipeline = __commonJS({
6569
6662
  });
6570
6663
  return streams.reduce(pipe);
6571
6664
  }
6572
- module2.exports = pipeline6;
6665
+ module2.exports = pipeline7;
6573
6666
  }
6574
6667
  });
6575
6668
 
@@ -7807,15 +7900,15 @@ var require_index_cjs = __commonJS({
7807
7900
  };
7808
7901
  }
7809
7902
  function wrapConversion(toModel, graph) {
7810
- const path21 = [graph[toModel].parent, toModel];
7903
+ const path20 = [graph[toModel].parent, toModel];
7811
7904
  let fn = convert$1[graph[toModel].parent][toModel];
7812
7905
  let cur = graph[toModel].parent;
7813
7906
  while (graph[cur].parent) {
7814
- path21.unshift(graph[cur].parent);
7907
+ path20.unshift(graph[cur].parent);
7815
7908
  fn = link(convert$1[graph[cur].parent][cur], fn);
7816
7909
  cur = graph[cur].parent;
7817
7910
  }
7818
- fn.conversion = path21;
7911
+ fn.conversion = path20;
7819
7912
  return fn;
7820
7913
  }
7821
7914
  function route(fromModel) {
@@ -8445,7 +8538,7 @@ var require_node2 = __commonJS({
8445
8538
  var require_tail_file = __commonJS({
8446
8539
  "../../node_modules/winston/lib/winston/tail-file.js"(exports2, module2) {
8447
8540
  "use strict";
8448
- var fs27 = require("fs");
8541
+ var fs28 = require("fs");
8449
8542
  var { StringDecoder } = require("string_decoder");
8450
8543
  var { Stream } = require_readable();
8451
8544
  function noop() {
@@ -8466,7 +8559,7 @@ var require_tail_file = __commonJS({
8466
8559
  stream.emit("end");
8467
8560
  stream.emit("close");
8468
8561
  };
8469
- fs27.open(options2.file, "a+", "0644", (err2, fd) => {
8562
+ fs28.open(options2.file, "a+", "0644", (err2, fd) => {
8470
8563
  if (err2) {
8471
8564
  if (!iter) {
8472
8565
  stream.emit("error", err2);
@@ -8478,10 +8571,10 @@ var require_tail_file = __commonJS({
8478
8571
  }
8479
8572
  (function read() {
8480
8573
  if (stream.destroyed) {
8481
- fs27.close(fd, noop);
8574
+ fs28.close(fd, noop);
8482
8575
  return;
8483
8576
  }
8484
- return fs27.read(fd, buffer, 0, buffer.length, pos, (error, bytes) => {
8577
+ return fs28.read(fd, buffer, 0, buffer.length, pos, (error, bytes) => {
8485
8578
  if (error) {
8486
8579
  if (!iter) {
8487
8580
  stream.emit("error", error);
@@ -8540,8 +8633,8 @@ var require_tail_file = __commonJS({
8540
8633
  var require_file = __commonJS({
8541
8634
  "../../node_modules/winston/lib/winston/transports/file.js"(exports2, module2) {
8542
8635
  "use strict";
8543
- var fs27 = require("fs");
8544
- var path21 = require("path");
8636
+ var fs28 = require("fs");
8637
+ var path20 = require("path");
8545
8638
  var asyncSeries = require_series();
8546
8639
  var zlib3 = require("zlib");
8547
8640
  var { MESSAGE } = require_triple_beam();
@@ -8571,14 +8664,14 @@ var require_file = __commonJS({
8571
8664
  this._onError = this._onError.bind(this);
8572
8665
  if (options2.filename || options2.dirname) {
8573
8666
  throwIf("filename or dirname", "stream");
8574
- this._basename = this.filename = options2.filename ? path21.basename(options2.filename) : "winston.log";
8575
- this.dirname = options2.dirname || path21.dirname(options2.filename);
8667
+ this._basename = this.filename = options2.filename ? path20.basename(options2.filename) : "winston.log";
8668
+ this.dirname = options2.dirname || path20.dirname(options2.filename);
8576
8669
  this.options = options2.options || { flags: "a" };
8577
8670
  } else if (options2.stream) {
8578
8671
  console.warn("options.stream will be removed in winston@4. Use winston.transports.Stream");
8579
8672
  throwIf("stream", "filename", "maxsize");
8580
8673
  this._dest = this._stream.pipe(this._setupStream(options2.stream));
8581
- this.dirname = path21.dirname(this._dest.path);
8674
+ this.dirname = path20.dirname(this._dest.path);
8582
8675
  } else {
8583
8676
  throw new Error("Cannot log to file without filename or stream.");
8584
8677
  }
@@ -8741,11 +8834,11 @@ var require_file = __commonJS({
8741
8834
  options2 = {};
8742
8835
  }
8743
8836
  options2 = normalizeQuery(options2);
8744
- const file = path21.join(this.dirname, this.filename);
8837
+ const file = path20.join(this.dirname, this.filename);
8745
8838
  let buff = "";
8746
8839
  let results = [];
8747
8840
  let row = 0;
8748
- const stream = fs27.createReadStream(file, {
8841
+ const stream = fs28.createReadStream(file, {
8749
8842
  encoding: "utf8"
8750
8843
  });
8751
8844
  stream.on("error", (err2) => {
@@ -8846,7 +8939,7 @@ var require_file = __commonJS({
8846
8939
  * TODO: Refactor me.
8847
8940
  */
8848
8941
  stream(options2 = {}) {
8849
- const file = path21.join(this.dirname, this.filename);
8942
+ const file = path20.join(this.dirname, this.filename);
8850
8943
  const stream = new Stream();
8851
8944
  const tail = {
8852
8945
  file,
@@ -8896,8 +8989,8 @@ var require_file = __commonJS({
8896
8989
  */
8897
8990
  stat(callback) {
8898
8991
  const target = this._getFile();
8899
- const fullpath = path21.join(this.dirname, target);
8900
- fs27.stat(fullpath, (err2, stat) => {
8992
+ const fullpath = path20.join(this.dirname, target);
8993
+ fs28.stat(fullpath, (err2, stat) => {
8901
8994
  if (err2 && err2.code === "ENOENT") {
8902
8995
  debug("ENOENT\xA0ok", fullpath);
8903
8996
  this.filename = target;
@@ -9000,9 +9093,9 @@ var require_file = __commonJS({
9000
9093
  * @returns {WritableStream} Stream that writes to disk for the active file.
9001
9094
  */
9002
9095
  _createStream(source) {
9003
- const fullpath = path21.join(this.dirname, this.filename);
9096
+ const fullpath = path20.join(this.dirname, this.filename);
9004
9097
  debug("create stream start", fullpath, this.options);
9005
- const dest = fs27.createWriteStream(fullpath, this.options).on("error", (err2) => debug(err2)).on("close", () => debug("close", dest.path, dest.bytesWritten)).on("open", () => {
9098
+ const dest = fs28.createWriteStream(fullpath, this.options).on("error", (err2) => debug(err2)).on("close", () => debug("close", dest.path, dest.bytesWritten)).on("open", () => {
9006
9099
  debug("file open ok", fullpath);
9007
9100
  this.emit("open", fullpath);
9008
9101
  source.pipe(dest);
@@ -9025,16 +9118,16 @@ var require_file = __commonJS({
9025
9118
  */
9026
9119
  _incFile(callback) {
9027
9120
  debug("_incFile", this.filename);
9028
- const ext = path21.extname(this._basename);
9029
- const basename = path21.basename(this._basename, ext);
9121
+ const ext = path20.extname(this._basename);
9122
+ const basename = path20.basename(this._basename, ext);
9030
9123
  const tasks = [];
9031
9124
  if (this.zippedArchive) {
9032
9125
  tasks.push(
9033
9126
  function(cb) {
9034
9127
  const num = this._created > 0 && !this.tailable ? this._created : "";
9035
9128
  this._compressFile(
9036
- path21.join(this.dirname, `${basename}${num}${ext}`),
9037
- path21.join(this.dirname, `${basename}${num}${ext}.gz`),
9129
+ path20.join(this.dirname, `${basename}${num}${ext}`),
9130
+ path20.join(this.dirname, `${basename}${num}${ext}.gz`),
9038
9131
  cb
9039
9132
  );
9040
9133
  }.bind(this)
@@ -9059,8 +9152,8 @@ var require_file = __commonJS({
9059
9152
  * @private
9060
9153
  */
9061
9154
  _getFile() {
9062
- const ext = path21.extname(this._basename);
9063
- const basename = path21.basename(this._basename, ext);
9155
+ const ext = path20.extname(this._basename);
9156
+ const basename = path20.basename(this._basename, ext);
9064
9157
  const isRotation = this.rotationFormat ? this.rotationFormat() : this._created;
9065
9158
  return !this.tailable && this._created ? `${basename}${isRotation}${ext}` : `${basename}${ext}`;
9066
9159
  }
@@ -9080,8 +9173,8 @@ var require_file = __commonJS({
9080
9173
  const isOldest = oldest !== 0 ? oldest : "";
9081
9174
  const isZipped = this.zippedArchive ? ".gz" : "";
9082
9175
  const filePath = `${basename}${isOldest}${ext}${isZipped}`;
9083
- const target = path21.join(this.dirname, filePath);
9084
- fs27.unlink(target, callback);
9176
+ const target = path20.join(this.dirname, filePath);
9177
+ fs28.unlink(target, callback);
9085
9178
  }
9086
9179
  /**
9087
9180
  * Roll files forward based on integer, up to maxFiles. e.g. if base if
@@ -9103,20 +9196,20 @@ var require_file = __commonJS({
9103
9196
  for (let x2 = this.maxFiles - 1; x2 > 1; x2--) {
9104
9197
  tasks.push(function(i6, cb) {
9105
9198
  let fileName = `${basename}${i6 - 1}${ext}${isZipped}`;
9106
- const tmppath = path21.join(this.dirname, fileName);
9107
- fs27.exists(tmppath, (exists) => {
9199
+ const tmppath = path20.join(this.dirname, fileName);
9200
+ fs28.exists(tmppath, (exists) => {
9108
9201
  if (!exists) {
9109
9202
  return cb(null);
9110
9203
  }
9111
9204
  fileName = `${basename}${i6}${ext}${isZipped}`;
9112
- fs27.rename(tmppath, path21.join(this.dirname, fileName), cb);
9205
+ fs28.rename(tmppath, path20.join(this.dirname, fileName), cb);
9113
9206
  });
9114
9207
  }.bind(this, x2));
9115
9208
  }
9116
9209
  asyncSeries(tasks, () => {
9117
- fs27.rename(
9118
- path21.join(this.dirname, `${basename}${ext}${isZipped}`),
9119
- path21.join(this.dirname, `${basename}1${ext}${isZipped}`),
9210
+ fs28.rename(
9211
+ path20.join(this.dirname, `${basename}${ext}${isZipped}`),
9212
+ path20.join(this.dirname, `${basename}1${ext}${isZipped}`),
9120
9213
  callback
9121
9214
  );
9122
9215
  });
@@ -9130,22 +9223,22 @@ var require_file = __commonJS({
9130
9223
  * @private
9131
9224
  */
9132
9225
  _compressFile(src, dest, callback) {
9133
- fs27.access(src, fs27.F_OK, (err2) => {
9226
+ fs28.access(src, fs28.F_OK, (err2) => {
9134
9227
  if (err2) {
9135
9228
  return callback();
9136
9229
  }
9137
9230
  var gzip = zlib3.createGzip();
9138
- var inp = fs27.createReadStream(src);
9139
- var out = fs27.createWriteStream(dest);
9231
+ var inp = fs28.createReadStream(src);
9232
+ var out = fs28.createWriteStream(dest);
9140
9233
  out.on("finish", () => {
9141
- fs27.unlink(src, callback);
9234
+ fs28.unlink(src, callback);
9142
9235
  });
9143
9236
  inp.pipe(gzip).pipe(out);
9144
9237
  });
9145
9238
  }
9146
9239
  _createLogDirIfNotExist(dirPath) {
9147
- if (!fs27.existsSync(dirPath)) {
9148
- fs27.mkdirSync(dirPath, { recursive: true });
9240
+ if (!fs28.existsSync(dirPath)) {
9241
+ fs28.mkdirSync(dirPath, { recursive: true });
9149
9242
  }
9150
9243
  }
9151
9244
  };
@@ -9229,9 +9322,9 @@ var require_http = __commonJS({
9229
9322
  };
9230
9323
  const auth = options2.params.auth || null;
9231
9324
  delete options2.params.auth;
9232
- const path21 = options2.params.path || null;
9325
+ const path20 = options2.params.path || null;
9233
9326
  delete options2.params.path;
9234
- this._request(options2, auth, path21, (err2, res, body) => {
9327
+ this._request(options2, auth, path20, (err2, res, body) => {
9235
9328
  if (res && res.statusCode !== 200) {
9236
9329
  err2 = new Error(`Invalid HTTP Status Code: ${res.statusCode}`);
9237
9330
  }
@@ -9259,12 +9352,12 @@ var require_http = __commonJS({
9259
9352
  method: "stream",
9260
9353
  params: options2
9261
9354
  };
9262
- const path21 = options2.params.path || null;
9355
+ const path20 = options2.params.path || null;
9263
9356
  delete options2.params.path;
9264
9357
  const auth = options2.params.auth || null;
9265
9358
  delete options2.params.auth;
9266
9359
  let buff = "";
9267
- const req = this._request(options2, auth, path21);
9360
+ const req = this._request(options2, auth, path20);
9268
9361
  stream.destroy = () => req.destroy();
9269
9362
  req.on("data", (data) => {
9270
9363
  data = (buff + data).split(/\n+/);
@@ -9290,14 +9383,14 @@ var require_http = __commonJS({
9290
9383
  * @param {string} path - request path
9291
9384
  * @param {function} callback - Continuation to respond to when complete.
9292
9385
  */
9293
- _request(options2, auth, path21, callback) {
9386
+ _request(options2, auth, path20, callback) {
9294
9387
  options2 = options2 || {};
9295
9388
  auth = auth || this.auth;
9296
- path21 = path21 || this.path || "";
9389
+ path20 = path20 || this.path || "";
9297
9390
  if (this.batch) {
9298
- this._doBatch(options2, callback, auth, path21);
9391
+ this._doBatch(options2, callback, auth, path20);
9299
9392
  } else {
9300
- this._doRequest(options2, callback, auth, path21);
9393
+ this._doRequest(options2, callback, auth, path20);
9301
9394
  }
9302
9395
  }
9303
9396
  /**
@@ -9307,18 +9400,18 @@ var require_http = __commonJS({
9307
9400
  * @param {Object?} auth - authentication options
9308
9401
  * @param {string} path - request path
9309
9402
  */
9310
- _doBatch(options2, callback, auth, path21) {
9403
+ _doBatch(options2, callback, auth, path20) {
9311
9404
  this.batchOptions.push(options2);
9312
9405
  if (this.batchOptions.length === 1) {
9313
9406
  const me3 = this;
9314
9407
  this.batchCallback = callback;
9315
9408
  this.batchTimeoutID = setTimeout(function() {
9316
9409
  me3.batchTimeoutID = -1;
9317
- me3._doBatchRequest(me3.batchCallback, auth, path21);
9410
+ me3._doBatchRequest(me3.batchCallback, auth, path20);
9318
9411
  }, this.batchInterval);
9319
9412
  }
9320
9413
  if (this.batchOptions.length === this.batchCount) {
9321
- this._doBatchRequest(this.batchCallback, auth, path21);
9414
+ this._doBatchRequest(this.batchCallback, auth, path20);
9322
9415
  }
9323
9416
  }
9324
9417
  /**
@@ -9327,14 +9420,14 @@ var require_http = __commonJS({
9327
9420
  * @param {Object?} auth - authentication options
9328
9421
  * @param {string} path - request path
9329
9422
  */
9330
- _doBatchRequest(callback, auth, path21) {
9423
+ _doBatchRequest(callback, auth, path20) {
9331
9424
  if (this.batchTimeoutID > 0) {
9332
9425
  clearTimeout(this.batchTimeoutID);
9333
9426
  this.batchTimeoutID = -1;
9334
9427
  }
9335
9428
  const batchOptionsCopy = this.batchOptions.slice();
9336
9429
  this.batchOptions = [];
9337
- this._doRequest(batchOptionsCopy, callback, auth, path21);
9430
+ this._doRequest(batchOptionsCopy, callback, auth, path20);
9338
9431
  }
9339
9432
  /**
9340
9433
  * Make a request to a winstond server or any http server which can
@@ -9344,7 +9437,7 @@ var require_http = __commonJS({
9344
9437
  * @param {Object?} auth - authentication options
9345
9438
  * @param {string} path - request path
9346
9439
  */
9347
- _doRequest(options2, callback, auth, path21) {
9440
+ _doRequest(options2, callback, auth, path20) {
9348
9441
  const headers = Object.assign({}, this.headers);
9349
9442
  if (auth && auth.bearer) {
9350
9443
  headers.Authorization = `Bearer ${auth.bearer}`;
@@ -9354,7 +9447,7 @@ var require_http = __commonJS({
9354
9447
  method: "POST",
9355
9448
  host: this.host,
9356
9449
  port: this.port,
9357
- path: `/${path21.replace(/^\//, "")}`,
9450
+ path: `/${path20.replace(/^\//, "")}`,
9358
9451
  headers,
9359
9452
  auth: auth && auth.username && auth.password ? `${auth.username}:${auth.password}` : "",
9360
9453
  agent: this.agent
@@ -10272,7 +10365,7 @@ var require_profiler = __commonJS({
10272
10365
  var require_logger = __commonJS({
10273
10366
  "../../node_modules/winston/lib/winston/logger.js"(exports2, module2) {
10274
10367
  "use strict";
10275
- var { Stream, Transform: Transform4 } = require_readable();
10368
+ var { Stream, Transform: Transform5 } = require_readable();
10276
10369
  var asyncForEach = require_forEach();
10277
10370
  var { LEVEL, SPLAT } = require_triple_beam();
10278
10371
  var isStream = require_is_stream();
@@ -10283,7 +10376,7 @@ var require_logger = __commonJS({
10283
10376
  var { warn } = require_common();
10284
10377
  var config = require_config2();
10285
10378
  var formatRegExp = /%[scdjifoO%]/g;
10286
- var Logger2 = class extends Transform4 {
10379
+ var Logger2 = class extends Transform5 {
10287
10380
  /**
10288
10381
  * Constructor function for the Logger object responsible for persisting log
10289
10382
  * messages and metadata to one or more transports.
@@ -11682,7 +11775,7 @@ var CONSTANTS, Constants_default;
11682
11775
  var init_Constants = __esm({
11683
11776
  "../../packages/constants/src/Constants.ts"() {
11684
11777
  CONSTANTS = {
11685
- cliVersion: "2.1.1",
11778
+ cliVersion: "2.2.0",
11686
11779
  backendVersion: 1,
11687
11780
  backendPort: 5088,
11688
11781
  workerVersion: 2,
@@ -11699,6 +11792,23 @@ var init_Constants = __esm({
11699
11792
  * for the whole project, and once per dimension when a worker compiles its line plan.
11700
11793
  */
11701
11794
  MASK_VALUES: ["hash", "mask", "crypt", "random", "seeded-random", "none"],
11795
+ /**
11796
+ * Where each kind of resource lives inside the `remora/` directory, as a path relative to it.
11797
+ *
11798
+ * Fixed, not configurable: project.json used to name these folders, which meant nothing outside
11799
+ * the loader could find a resource from its path alone, and two projects could disagree about
11800
+ * what a remora project looks like. `sources`, `producers` and `consumers` must exist; `schemas`
11801
+ * and `codeSets` are read only when the folder is there.
11802
+ */
11803
+ RESOURCE_FOLDERS: {
11804
+ sources: "/sources",
11805
+ producers: "/producers",
11806
+ consumers: "/consumers",
11807
+ schemas: "/schemas",
11808
+ codeSets: "/code_sets"
11809
+ },
11810
+ /** project.json keys that used to name the resource folders, kept only to warn on and ignore. */
11811
+ LEGACY_PROJECT_PATH_KEYS: ["sources", "producers", "consumers", "schemas", "codeSets"],
11702
11812
  defaults: {
11703
11813
  REMORA_PATH: "./remora",
11704
11814
  PRODUCER_TEMP_FOLDER: ".temp",
@@ -11777,6 +11887,19 @@ var init_Constants = __esm({
11777
11887
  * than memory — a zip bomb stops with a named error instead of filling the scope folder.
11778
11888
  */
11779
11889
  MAX_ARCHIVE_ENTRIES: 1e4,
11890
+ /**
11891
+ * Files one consumer's output may be split into. Generous for a country, a region or a year,
11892
+ * and low enough to catch "split by customer_id" before it exhausts the process's descriptor
11893
+ * table. The run fails mid-pass with a named error rather than producing a directory nobody
11894
+ * asked for.
11895
+ */
11896
+ MAX_SPLIT_FILES: 1e3,
11897
+ /**
11898
+ * Write streams the split pass may hold open at once, independent of how many files it ends
11899
+ * up writing. Past this the least recently written bucket is closed and reopened in append
11900
+ * mode when its next line arrives — its byte counter lives in the bucket, not the stream.
11901
+ */
11902
+ MAX_OPEN_SPLIT_STREAMS: 64,
11780
11903
  /**
11781
11904
  * Rows a sample may ask for out of the pipeline. A sample is a look at the data, never an
11782
11905
  * export of it — `remora sample -s` and the preview's Data tab are both held to this.
@@ -12804,6 +12927,7 @@ var init_Validator = __esm({
12804
12927
  }
12805
12928
  }
12806
12929
  errors.push(...this.validateOmitNull(consumer));
12930
+ errors.push(...this.validateSplit(consumer));
12807
12931
  const dimensionFields = consumer.fields.filter((x2) => x2.key !== "*" && !x2.fixed && !x2.copyFrom && !x2.lineCount);
12808
12932
  const dimensionKeys = dimensionFields.map((x2) => `${x2.from ?? "_default_"}::${x2.key}`);
12809
12933
  const duplicateDimensionKeys = dimensionKeys.filter((k7, i6) => dimensionKeys.indexOf(k7) !== i6);
@@ -12860,6 +12984,42 @@ var init_Validator = __esm({
12860
12984
  }
12861
12985
  return errors;
12862
12986
  };
12987
+ /**
12988
+ * Checks `split`, which breaks a consumer's output into several files — by the value of one or
12989
+ * more fields, by a size cap, or both.
12990
+ *
12991
+ * The alias-resolution and wildcard rules follow `distinctOn`: the keys name consumer fields as
12992
+ * the output writes them, and a consumer selecting `*` has no field list to check them against
12993
+ * until it reads its producers. A field it declares beside the `*` is still checked, since a
12994
+ * hidden one is hidden whatever the wildcard expands to.
12995
+ */
12996
+ this.validateSplit = (consumer) => {
12997
+ const errors = [];
12998
+ const { split: split2 } = consumer;
12999
+ const positionalFormats = ["CSV", "TXT", "PARQUET"];
13000
+ if (!Algo_default.hasVal(split2)) return errors;
13001
+ if (Algo_default.hasVal(consumer.maximumFileSize))
13002
+ errors.push(`Consumer "${consumer.name}" sets both "maximumFileSize" and "split". They are the same cap: move the value to "split": { "maxFileSizeGb": ${consumer.maximumFileSize} } and remove "maximumFileSize".`);
13003
+ const keys = split2.by ?? [];
13004
+ if (keys.length === 0 && !Algo_default.hasVal(split2.maxFileSizeGb))
13005
+ errors.push(`The "split" of consumer "${consumer.name}" sets neither "by" nor "maxFileSizeGb", so it would split nothing. Remove it, or say how the output should be split.`);
13006
+ if (Algo_default.hasVal(split2.maxFileSizeGb) && !(split2.maxFileSizeGb > 0))
13007
+ errors.push(`The "split.maxFileSizeGb" of consumer "${consumer.name}" must be greater than zero (got ${split2.maxFileSizeGb}).`);
13008
+ const apiOutputs = (consumer.outputs ?? []).filter((x2) => x2.format.toUpperCase() === "API");
13009
+ if (apiOutputs.length > 0)
13010
+ errors.push(`Consumer "${consumer.name}" declares "split", but an API output is a response, not a file, and there is nothing to split it into.`);
13011
+ if (keys.length === 0) return errors;
13012
+ const fieldKeys = (consumer.fields ?? []).map((x2) => x2.alias ?? x2.key);
13013
+ const missingKeys = fieldKeys.includes("*") ? [] : keys.filter((key) => !fieldKeys.includes(key));
13014
+ if (missingKeys.length > 0)
13015
+ errors.push(`"split.by" references key(s) "${missingKeys.join(", ")}" that are not present in the consumer "${consumer.name}".`);
13016
+ const positional = (consumer.outputs ?? []).filter((x2) => positionalFormats.includes(x2.format.toUpperCase()));
13017
+ if (positional.length === 0) return errors;
13018
+ const hiddenKeys = keys.filter((key) => (consumer.fields ?? []).some((x2) => (x2.alias ?? x2.key) === key && x2.hidden));
13019
+ if (hiddenKeys.length > 0)
13020
+ 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.`);
13021
+ return errors;
13022
+ };
12863
13023
  /**
12864
13024
  * Checks `omitNull`, which drops a record's null fields on export. It only means anything where a
12865
13025
  * record carries its own field names, and it is applied when the record is serialized — one record
@@ -12956,7 +13116,7 @@ var init_Validator = __esm({
12956
13116
  */
12957
13117
  this.detectOnFinishRecursion = (consumer) => {
12958
13118
  const errors = [];
12959
- const path21 = [];
13119
+ const path20 = [];
12960
13120
  const targetsOf = (cons) => {
12961
13121
  const names = [];
12962
13122
  for (const output of cons.outputs ?? []) {
@@ -12968,17 +13128,17 @@ var init_Validator = __esm({
12968
13128
  return names;
12969
13129
  };
12970
13130
  const walk = (name) => {
12971
- if (path21.some((x2) => x2.toLowerCase() === name.toLowerCase())) {
12972
- errors.push(`Consumer "${consumer.name}" has a cyclic run-consumer (onSuccess/onError) chain: ${[...path21, name].join(" -> ")}`);
13131
+ if (path20.some((x2) => x2.toLowerCase() === name.toLowerCase())) {
13132
+ errors.push(`Consumer "${consumer.name}" has a cyclic run-consumer (onSuccess/onError) chain: ${[...path20, name].join(" -> ")}`);
12973
13133
  return true;
12974
13134
  }
12975
13135
  const cons = Environment_default.getConsumer(name);
12976
13136
  if (!cons) return false;
12977
- path21.push(name);
13137
+ path20.push(name);
12978
13138
  for (const target of targetsOf(cons)) {
12979
13139
  if (walk(target)) return true;
12980
13140
  }
12981
- path21.pop();
13141
+ path20.pop();
12982
13142
  return false;
12983
13143
  };
12984
13144
  walk(consumer.name);
@@ -13563,7 +13723,7 @@ ${errors.map((x2) => ` - ${x2}`).join("\n")}`);
13563
13723
  });
13564
13724
 
13565
13725
  // ../../packages/common/src/Environment.ts
13566
- var import_fs4, import_crypto, import_adm_zip, import_path4, import_client_s3, RESOURCE_SCHEMA_KEYS, INVALID_RESOURCE_MESSAGES, EnvironmentClass, Environment, Environment_default;
13726
+ var import_fs4, import_crypto, import_adm_zip, import_path4, import_client_s3, RESOURCE_SCHEMA_KEYS, INVALID_RESOURCE_MESSAGES, STAGING_DIRECTORY, EnvironmentClass, Environment, Environment_default;
13567
13727
  var init_Environment = __esm({
13568
13728
  "../../packages/common/src/Environment.ts"() {
13569
13729
  import_fs4 = __toESM(require("fs"), 1);
@@ -13592,6 +13752,7 @@ var init_Environment = __esm({
13592
13752
  codeSet: "Invalid code set configuration",
13593
13753
  schema: "Invalid schema configuration"
13594
13754
  };
13755
+ STAGING_DIRECTORY = ".remora-incoming";
13595
13756
  EnvironmentClass = class {
13596
13757
  constructor() {
13597
13758
  this._env = null;
@@ -13671,31 +13832,72 @@ var init_Environment = __esm({
13671
13832
  Bucket: bucketName,
13672
13833
  Key: key
13673
13834
  });
13835
+ let buffer;
13674
13836
  try {
13675
13837
  const response = await s3Client.send(command5);
13676
- return this._saveConfigFile(Buffer.from(await response.Body.transformToByteArray()), url);
13838
+ buffer = Buffer.from(await response.Body.transformToByteArray());
13677
13839
  } catch (error) {
13678
- throw new Error(`Error at bucket ${bucketName}, while obtainign ${key}, caused ${error}`);
13840
+ throw new Error(`Error at bucket ${bucketName}, while obtaining ${key}, caused ${error}`);
13679
13841
  }
13842
+ return this._saveConfigFile(buffer, url);
13680
13843
  };
13681
- // save the configuration files on the local remora path
13844
+ /**
13845
+ * Extract a downloaded configuration archive over the local remora path.
13846
+ *
13847
+ * The archive is fully read and extracted to a staging directory BEFORE anything live is
13848
+ * touched, because the destructive step is not reversible: a configuration that fails to
13849
+ * extract half way through would otherwise leave the process running against an empty
13850
+ * project directory until the next successful download.
13851
+ */
13682
13852
  this._saveConfigFile = (buffer, url) => {
13683
- const zip = new import_adm_zip.default(buffer);
13853
+ Affirm_default(buffer?.length, `Empty configuration archive downloaded from ${url}`);
13854
+ const zip = this._readArchive(buffer, url);
13684
13855
  const extractPath = import_path4.default.join(this._localPath, "remora");
13685
- if (import_fs4.default.existsSync(extractPath)) {
13686
- for (const file of import_fs4.default.readdirSync(extractPath)) {
13687
- if (file === "temp") continue;
13688
- const filePath = import_path4.default.join(extractPath, file);
13689
- if (import_fs4.default.statSync(filePath).isDirectory())
13690
- import_fs4.default.rmSync(filePath, { recursive: true, force: true });
13691
- else
13692
- import_fs4.default.unlinkSync(filePath);
13693
- }
13856
+ const stagingPath = import_path4.default.join(this._localPath, STAGING_DIRECTORY);
13857
+ import_fs4.default.rmSync(stagingPath, { recursive: true, force: true });
13858
+ try {
13859
+ zip.extractAllTo(stagingPath, true);
13860
+ this._clearConfigDirectory(extractPath);
13861
+ for (const entry of import_fs4.default.readdirSync(stagingPath))
13862
+ import_fs4.default.renameSync(import_path4.default.join(stagingPath, entry), import_path4.default.join(extractPath, entry));
13863
+ } finally {
13864
+ import_fs4.default.rmSync(stagingPath, { recursive: true, force: true });
13694
13865
  }
13695
- zip.extractAllTo(extractPath, true);
13696
13866
  Logger_default.log(`Remote configuration downloaded and extracted from ${url}`);
13697
13867
  return true;
13698
13868
  };
13869
+ /**
13870
+ * Read the archive's entry table up front. adm-zip parses entries lazily, so without this the
13871
+ * first read of a malformed archive happens inside `extractAllTo` — after the live
13872
+ * configuration has already been removed.
13873
+ */
13874
+ this._readArchive = (buffer, url) => {
13875
+ try {
13876
+ const zip = new import_adm_zip.default(buffer);
13877
+ Affirm_default.hasItems(zip.getEntries(), `Configuration archive from ${url} contains no files`);
13878
+ return zip;
13879
+ } catch (error) {
13880
+ const reason = error instanceof Error ? error.message : String(error);
13881
+ throw new Error(
13882
+ `Unreadable configuration archive from ${url}: ${reason}. Rebuild the archive from the project directory (\`remora package\`) instead of adding files to the existing one \u2014 appending leaves two entries under the same name, which is rejected because the two could hold different content.`
13883
+ );
13884
+ }
13885
+ };
13886
+ /** Remove everything the previous configuration left behind, keeping the run scratch directory. */
13887
+ this._clearConfigDirectory = (extractPath) => {
13888
+ if (!import_fs4.default.existsSync(extractPath)) {
13889
+ import_fs4.default.mkdirSync(extractPath, { recursive: true });
13890
+ return;
13891
+ }
13892
+ for (const file of import_fs4.default.readdirSync(extractPath)) {
13893
+ if (file === "temp") continue;
13894
+ const filePath = import_path4.default.join(extractPath, file);
13895
+ if (import_fs4.default.statSync(filePath).isDirectory())
13896
+ import_fs4.default.rmSync(filePath, { recursive: true, force: true });
13897
+ else
13898
+ import_fs4.default.unlinkSync(filePath);
13899
+ }
13900
+ };
13699
13901
  /**
13700
13902
  * Check if the configuration has changed and reload if so.
13701
13903
  * For remote URIs, re-downloads using ETag caching (HTTP 304 avoids redundant downloads).
@@ -13758,25 +13960,26 @@ var init_Environment = __esm({
13758
13960
  if (!projectConfig) return this._emptyInspection(problems);
13759
13961
  SchemaValidator_default.unloadSchemas();
13760
13962
  const read = (entries) => this._readConfigs(envPath, entries, remoraPath, problems);
13761
- const schemaPaths = (projectConfig.schemas ?? []).filter((x2) => import_fs4.default.existsSync(import_path4.default.join(envPath, x2)));
13762
- const schemaFiles = read(schemaPaths);
13963
+ const optional = (folder) => import_fs4.default.existsSync(import_path4.default.join(envPath, folder)) ? [folder] : [];
13964
+ const folders = Constants_default.RESOURCE_FOLDERS;
13965
+ const schemaFiles = read(optional(folders.schemas));
13763
13966
  const schemas = schemaFiles.map((x2) => x2.config);
13764
13967
  schemas.forEach((schema) => this._registerSchema(schema, problems));
13765
- const sourceFiles = read(projectConfig.sources);
13968
+ const sourceFiles = read([folders.sources]);
13766
13969
  const sources = sourceFiles.map((x2) => x2.config);
13767
13970
  sourceFiles.forEach((source) => this._recordSchemaProblem(source, "source", problems));
13768
13971
  this._checkApiQueueSource(projectConfig, sources, problems);
13769
- const producerFiles = read(projectConfig.producers);
13972
+ const producerFiles = read([folders.producers]);
13770
13973
  const producers = producerFiles.map((x2) => x2.config);
13771
13974
  producerFiles.forEach(({ config: producer, file }) => {
13772
13975
  const schemaProblem = this._schemaProblem(producer, "producer", file);
13773
13976
  this._expandProducer(producer, problems);
13774
13977
  if (schemaProblem) problems.push(schemaProblem);
13775
13978
  });
13776
- const consumerFiles = read(projectConfig.consumers);
13979
+ const consumerFiles = read([folders.consumers]);
13777
13980
  const consumers = consumerFiles.map((x2) => x2.config);
13778
13981
  consumerFiles.forEach((consumer) => this._recordSchemaProblem(consumer, "consumer", problems));
13779
- const codeSetFiles = read(projectConfig.codeSets ?? []);
13982
+ const codeSetFiles = read(optional(folders.codeSets));
13780
13983
  const codeSets = codeSetFiles.map((x2) => x2.config);
13781
13984
  codeSetFiles.forEach((codeSet) => this._recordSchemaProblem(codeSet, "codeSet", problems));
13782
13985
  this._checkCodeSetNames(codeSets, problems);
@@ -13826,6 +14029,7 @@ var init_Environment = __esm({
13826
14029
  const { env: env2, projectName, projectSettings, problems } = this.inspect(remoraPath);
13827
14030
  const fatal = problems.find((x2) => x2.severity === "error");
13828
14031
  if (fatal) throw new Error(fatal.message);
14032
+ problems.filter((x2) => x2.severity === "warning").forEach((x2) => Logger_default.warn(x2.message));
13829
14033
  this._projectName = projectName;
13830
14034
  this._projectSettings = projectSettings;
13831
14035
  this.init(env2);
@@ -13861,8 +14065,27 @@ var init_Environment = __esm({
13861
14065
  message: `Invalid project configuration - ${SchemaErrorReporter_default.describe(validation.errors, projectConfig)}`
13862
14066
  });
13863
14067
  }
14068
+ this._checkLegacyPaths(projectConfig, projectPath, problems);
13864
14069
  return projectConfig;
13865
14070
  };
14071
+ /**
14072
+ * project.json used to name the folder each kind of resource was read from. It no longer does —
14073
+ * the folders are fixed — so the old keys are reported and ignored.
14074
+ *
14075
+ * A WARNING, not an error: a project written against an older CLI still runs, and the author is
14076
+ * told once, per load, what to delete. This is the one warning the loader raises.
14077
+ */
14078
+ this._checkLegacyPaths = (projectConfig, projectPath, problems) => {
14079
+ const stale = Constants_default.LEGACY_PROJECT_PATH_KEYS.filter((key) => key in (projectConfig ?? {}));
14080
+ if (stale.length === 0) return;
14081
+ const folders = Object.values(Constants_default.RESOURCE_FOLDERS).join(", ");
14082
+ problems.push({
14083
+ severity: "warning",
14084
+ scope: "project",
14085
+ file: projectPath,
14086
+ message: `Project configuration: ${stale.map((x2) => `"${x2}"`).join(", ")} no longer decide where resources live and can be removed - the folders are fixed (${folders})`
14087
+ });
14088
+ };
13866
14089
  /**
13867
14090
  * Reads every JSON file the entries point at.
13868
14091
  *
@@ -15028,14 +15251,14 @@ var init_readFile = __esm({
15028
15251
  import_promises3 = require("fs/promises");
15029
15252
  filePromises = {};
15030
15253
  fileIntercept = {};
15031
- readFile2 = (path21, options2) => {
15032
- if (fileIntercept[path21] !== void 0) {
15033
- return fileIntercept[path21];
15254
+ readFile2 = (path20, options2) => {
15255
+ if (fileIntercept[path20] !== void 0) {
15256
+ return fileIntercept[path20];
15034
15257
  }
15035
- if (!filePromises[path21] || options2?.ignoreCache) {
15036
- filePromises[path21] = (0, import_promises3.readFile)(path21, "utf8");
15258
+ if (!filePromises[path20] || options2?.ignoreCache) {
15259
+ filePromises[path20] = (0, import_promises3.readFile)(path20, "utf8");
15037
15260
  }
15038
- return filePromises[path21];
15261
+ return filePromises[path20];
15039
15262
  };
15040
15263
  }
15041
15264
  });
@@ -15147,8 +15370,8 @@ var init_externalDataInterceptor = __esm({
15147
15370
  getFileRecord() {
15148
15371
  return fileIntercept;
15149
15372
  },
15150
- interceptFile(path21, contents) {
15151
- fileIntercept[path21] = Promise.resolve(contents);
15373
+ interceptFile(path20, contents) {
15374
+ fileIntercept[path20] = Promise.resolve(contents);
15152
15375
  },
15153
15376
  getTokenRecord() {
15154
15377
  return tokenIntercept;
@@ -18228,8 +18451,8 @@ var init_createConfigValueProvider = __esm({
18228
18451
  return endpoint.url.href;
18229
18452
  }
18230
18453
  if ("hostname" in endpoint) {
18231
- const { protocol, hostname, port, path: path21 } = endpoint;
18232
- return `${protocol}//${hostname}${port ? ":" + port : ""}${path21}`;
18454
+ const { protocol, hostname, port, path: path20 } = endpoint;
18455
+ return `${protocol}//${hostname}${port ? ":" + port : ""}${path20}`;
18233
18456
  }
18234
18457
  }
18235
18458
  return endpoint;
@@ -18604,18 +18827,18 @@ var getAttrPathList;
18604
18827
  var init_getAttrPathList = __esm({
18605
18828
  "../../node_modules/@smithy/core/dist-es/submodules/endpoints/util-endpoints/lib/getAttrPathList.js"() {
18606
18829
  init_types2();
18607
- getAttrPathList = (path21) => {
18608
- const parts = path21.split(".");
18830
+ getAttrPathList = (path20) => {
18831
+ const parts = path20.split(".");
18609
18832
  const pathList = [];
18610
18833
  for (const part of parts) {
18611
18834
  const squareBracketIndex = part.indexOf("[");
18612
18835
  if (squareBracketIndex !== -1) {
18613
18836
  if (part.indexOf("]") !== part.length - 1) {
18614
- throw new EndpointError(`Path: '${path21}' does not end with ']'`);
18837
+ throw new EndpointError(`Path: '${path20}' does not end with ']'`);
18615
18838
  }
18616
18839
  const arrayIndex = part.slice(squareBracketIndex + 1, -1);
18617
18840
  if (Number.isNaN(parseInt(arrayIndex))) {
18618
- throw new EndpointError(`Invalid array index: '${arrayIndex}' in path: '${path21}'`);
18841
+ throw new EndpointError(`Invalid array index: '${arrayIndex}' in path: '${path20}'`);
18619
18842
  }
18620
18843
  if (squareBracketIndex !== 0) {
18621
18844
  pathList.push(part.slice(0, squareBracketIndex));
@@ -18636,9 +18859,9 @@ var init_getAttr = __esm({
18636
18859
  "../../node_modules/@smithy/core/dist-es/submodules/endpoints/util-endpoints/lib/getAttr.js"() {
18637
18860
  init_types2();
18638
18861
  init_getAttrPathList();
18639
- getAttr = (value, path21) => getAttrPathList(path21).reduce((acc, index) => {
18862
+ getAttr = (value, path20) => getAttrPathList(path20).reduce((acc, index) => {
18640
18863
  if (typeof acc !== "object") {
18641
- throw new EndpointError(`Index '${index}' in '${path21}' not found in '${JSON.stringify(value)}'`);
18864
+ throw new EndpointError(`Index '${index}' in '${path20}' not found in '${JSON.stringify(value)}'`);
18642
18865
  } else if (Array.isArray(acc)) {
18643
18866
  const i6 = parseInt(index);
18644
18867
  return acc[i6 < 0 ? acc.length + i6 : i6];
@@ -18699,8 +18922,8 @@ var init_parseURL = __esm({
18699
18922
  return value;
18700
18923
  }
18701
18924
  if (typeof value === "object" && "hostname" in value) {
18702
- const { hostname: hostname2, port, protocol: protocol2 = "", path: path21 = "", query = {} } = value;
18703
- const url = new URL(`${protocol2}//${hostname2}${port ? `:${port}` : ""}${path21}`);
18925
+ const { hostname: hostname2, port, protocol: protocol2 = "", path: path20 = "", query = {} } = value;
18926
+ const url = new URL(`${protocol2}//${hostname2}${port ? `:${port}` : ""}${path20}`);
18704
18927
  url.search = Object.entries(query).map(([k7, v3]) => `${k7}=${v3}`).join("&");
18705
18928
  return url;
18706
18929
  }
@@ -21044,11 +21267,11 @@ var init_HttpBindingProtocol = __esm({
21044
21267
  const opTraits = translateTraits(operationSchema.traits);
21045
21268
  if (opTraits.http) {
21046
21269
  request.method = opTraits.http[0];
21047
- const [path21, search] = opTraits.http[1].split("?");
21270
+ const [path20, search] = opTraits.http[1].split("?");
21048
21271
  if (request.path == "/") {
21049
- request.path = path21;
21272
+ request.path = path20;
21050
21273
  } else {
21051
- request.path += path21;
21274
+ request.path += path20;
21052
21275
  }
21053
21276
  const traitSearchParams = new URLSearchParams(search ?? "");
21054
21277
  for (const [key, value] of traitSearchParams) {
@@ -24761,12 +24984,12 @@ or increase socketAcquisitionWarningTimeout=(millis) in the NodeHttpHandler conf
24761
24984
  const password = request.password ?? "";
24762
24985
  auth = `${username}:${password}`;
24763
24986
  }
24764
- let path21 = request.path;
24987
+ let path20 = request.path;
24765
24988
  if (queryString) {
24766
- path21 += `?${queryString}`;
24989
+ path20 += `?${queryString}`;
24767
24990
  }
24768
24991
  if (request.fragment) {
24769
- path21 += `#${request.fragment}`;
24992
+ path20 += `#${request.fragment}`;
24770
24993
  }
24771
24994
  let hostname = request.hostname ?? "";
24772
24995
  if (hostname[0] === "[" && hostname.endsWith("]")) {
@@ -24778,7 +25001,7 @@ or increase socketAcquisitionWarningTimeout=(millis) in the NodeHttpHandler conf
24778
25001
  headers: request.headers,
24779
25002
  host: hostname,
24780
25003
  method: request.method,
24781
- path: path21,
25004
+ path: path20,
24782
25005
  port: request.port,
24783
25006
  agent,
24784
25007
  auth
@@ -25687,10 +25910,10 @@ ${longDate}
25687
25910
  ${credentialScope}
25688
25911
  ${toHex(hashedRequest)}`;
25689
25912
  }
25690
- getCanonicalPath({ path: path21 }) {
25913
+ getCanonicalPath({ path: path20 }) {
25691
25914
  if (this.uriEscapePath) {
25692
25915
  const normalizedPathSegments = [];
25693
- for (const pathSegment of path21.split("/")) {
25916
+ for (const pathSegment of path20.split("/")) {
25694
25917
  if (pathSegment?.length === 0)
25695
25918
  continue;
25696
25919
  if (pathSegment === ".")
@@ -25701,11 +25924,11 @@ ${toHex(hashedRequest)}`;
25701
25924
  normalizedPathSegments.push(pathSegment);
25702
25925
  }
25703
25926
  }
25704
- const normalizedPath = `${path21?.startsWith("/") ? "/" : ""}${normalizedPathSegments.join("/")}${normalizedPathSegments.length > 0 && path21?.endsWith("/") ? "/" : ""}`;
25927
+ const normalizedPath = `${path20?.startsWith("/") ? "/" : ""}${normalizedPathSegments.join("/")}${normalizedPathSegments.length > 0 && path20?.endsWith("/") ? "/" : ""}`;
25705
25928
  const doubleEncoded = escapeUri(normalizedPath);
25706
25929
  return doubleEncoded.replace(/%2F/g, "/");
25707
25930
  }
25708
- return path21;
25931
+ return path20;
25709
25932
  }
25710
25933
  validateResolvedCredentials(credentials) {
25711
25934
  if (typeof credentials !== "object" || typeof credentials.accessKeyId !== "string" || typeof credentials.secretAccessKey !== "string") {
@@ -33416,10 +33639,10 @@ var require_pend = __commonJS({
33416
33639
  // ../../node_modules/yauzl/fd-slicer.js
33417
33640
  var require_fd_slicer = __commonJS({
33418
33641
  "../../node_modules/yauzl/fd-slicer.js"(exports2) {
33419
- var fs27 = require("fs");
33642
+ var fs28 = require("fs");
33420
33643
  var util = require("util");
33421
33644
  var stream = require("stream");
33422
- var Readable8 = stream.Readable;
33645
+ var Readable10 = stream.Readable;
33423
33646
  var PassThrough = stream.PassThrough;
33424
33647
  var Pend = require_pend();
33425
33648
  var EventEmitter = require("events").EventEmitter;
@@ -33436,7 +33659,7 @@ var require_fd_slicer = __commonJS({
33436
33659
  FdSlicer.prototype.read = function(buffer, offset, length, position, callback) {
33437
33660
  var self2 = this;
33438
33661
  self2.pend.go(function(cb) {
33439
- fs27.read(self2.fd, buffer, offset, length, position, function(err2, bytesRead, buffer2) {
33662
+ fs28.read(self2.fd, buffer, offset, length, position, function(err2, bytesRead, buffer2) {
33440
33663
  cb();
33441
33664
  callback(err2, bytesRead, buffer2);
33442
33665
  });
@@ -33453,7 +33676,7 @@ var require_fd_slicer = __commonJS({
33453
33676
  self2.refCount -= 1;
33454
33677
  if (self2.refCount < 0) throw new Error("invalid unref");
33455
33678
  if (self2.refCount > 0) return;
33456
- fs27.close(self2.fd, onCloseDone);
33679
+ fs28.close(self2.fd, onCloseDone);
33457
33680
  function onCloseDone(err2) {
33458
33681
  if (err2) {
33459
33682
  self2.emit("error", err2);
@@ -33462,10 +33685,10 @@ var require_fd_slicer = __commonJS({
33462
33685
  }
33463
33686
  }
33464
33687
  };
33465
- util.inherits(ReadStream2, Readable8);
33688
+ util.inherits(ReadStream2, Readable10);
33466
33689
  function ReadStream2(context, options2) {
33467
33690
  options2 = options2 || {};
33468
- Readable8.call(this, options2);
33691
+ Readable10.call(this, options2);
33469
33692
  this.context = context;
33470
33693
  this.context.ref();
33471
33694
  this.start = options2.start || 0;
@@ -33485,7 +33708,7 @@ var require_fd_slicer = __commonJS({
33485
33708
  }
33486
33709
  self2.context.pend.go(function(cb) {
33487
33710
  var buffer = Buffer.allocUnsafe(toRead);
33488
- fs27.read(self2.context.fd, buffer, 0, toRead, self2.pos, function(err2, bytesRead) {
33711
+ fs28.read(self2.context.fd, buffer, 0, toRead, self2.pos, function(err2, bytesRead) {
33489
33712
  if (err2) {
33490
33713
  self2.destroy(err2);
33491
33714
  } else if (bytesRead === 0) {
@@ -33845,12 +34068,12 @@ var require_crc32 = __commonJS({
33845
34068
  // ../../node_modules/yauzl/index.js
33846
34069
  var require_yauzl = __commonJS({
33847
34070
  "../../node_modules/yauzl/index.js"(exports2) {
33848
- var fs27 = require("fs");
34071
+ var fs28 = require("fs");
33849
34072
  var zlib3 = require("zlib");
33850
34073
  var fd_slicer = require_fd_slicer();
33851
34074
  var util = require("util");
33852
34075
  var EventEmitter = require("events").EventEmitter;
33853
- var Transform4 = require("stream").Transform;
34076
+ var Transform5 = require("stream").Transform;
33854
34077
  var PassThrough = require("stream").PassThrough;
33855
34078
  var Writable2 = require("stream").Writable;
33856
34079
  var crc322 = typeof zlib3.crc32 === "function" ? zlib3.crc32 : require_crc32();
@@ -33870,9 +34093,9 @@ var require_yauzl = __commonJS({
33870
34093
  exports2.Entry = Entry;
33871
34094
  exports2.LocalFileHeader = LocalFileHeader;
33872
34095
  exports2.RandomAccessReader = RandomAccessReader;
33873
- function openPromise(path21, options2) {
34096
+ function openPromise(path20, options2) {
33874
34097
  return new Promise((resolve, reject) => {
33875
- open(path21, { ...options2, lazyEntries: true }, function(err2, zipfile) {
34098
+ open(path20, { ...options2, lazyEntries: true }, function(err2, zipfile) {
33876
34099
  if (err2) return reject(err2);
33877
34100
  resolve(zipfile);
33878
34101
  });
@@ -33902,7 +34125,7 @@ var require_yauzl = __commonJS({
33902
34125
  });
33903
34126
  });
33904
34127
  }
33905
- function open(path21, options2, callback) {
34128
+ function open(path20, options2, callback) {
33906
34129
  if (typeof options2 === "function") {
33907
34130
  callback = options2;
33908
34131
  options2 = null;
@@ -33914,10 +34137,10 @@ var require_yauzl = __commonJS({
33914
34137
  if (options2.validateEntrySizes == null) options2.validateEntrySizes = true;
33915
34138
  if (options2.strictFileNames == null) options2.strictFileNames = false;
33916
34139
  if (callback == null) callback = defaultCallback;
33917
- fs27.open(path21, "r", function(err2, fd) {
34140
+ fs28.open(path20, "r", function(err2, fd) {
33918
34141
  if (err2) return callback(err2);
33919
34142
  fromFd(fd, options2, function(err3, zipfile) {
33920
- if (err3) fs27.close(fd, defaultCallback);
34143
+ if (err3) fs28.close(fd, defaultCallback);
33921
34144
  callback(err3, zipfile);
33922
34145
  });
33923
34146
  });
@@ -33934,7 +34157,7 @@ var require_yauzl = __commonJS({
33934
34157
  if (options2.validateEntrySizes == null) options2.validateEntrySizes = true;
33935
34158
  if (options2.strictFileNames == null) options2.strictFileNames = false;
33936
34159
  if (callback == null) callback = defaultCallback;
33937
- fs27.fstat(fd, function(err2, stats) {
34160
+ fs28.fstat(fd, function(err2, stats) {
33938
34161
  if (err2) return callback(err2);
33939
34162
  var reader = new fd_slicer.FdSlicer(fd);
33940
34163
  fromRandomAccessReader(reader, stats.size, options2, callback);
@@ -34544,9 +34767,9 @@ var require_yauzl = __commonJS({
34544
34767
  callback();
34545
34768
  });
34546
34769
  }
34547
- util.inherits(AssertByteCountStream, Transform4);
34770
+ util.inherits(AssertByteCountStream, Transform5);
34548
34771
  function AssertByteCountStream(byteCount) {
34549
- Transform4.call(this);
34772
+ Transform5.call(this);
34550
34773
  this.actualByteCount = 0;
34551
34774
  this.expectedByteCount = byteCount;
34552
34775
  }
@@ -38161,8 +38384,8 @@ var init_HttpClient = __esm({
38161
38384
  throw new Error(err2.message ?? "An error occurred on GET.");
38162
38385
  }
38163
38386
  };
38164
- this.getWorkerUrl = (path21) => {
38165
- const cleanPath = path21.startsWith("/") ? path21.substring(1) : path21;
38387
+ this.getWorkerUrl = (path20) => {
38388
+ const cleanPath = path20.startsWith("/") ? path20.substring(1) : path20;
38166
38389
  return `${this.workerHost}/${cleanPath}`;
38167
38390
  };
38168
38391
  this.workerHost = ProcessENVManager_default.getEnvVariable("REMORA_WORKER_HOST") || "http://worker:5069";
@@ -44053,7 +44276,7 @@ var init_DeltaShareDriver = __esm({
44053
44276
  * disagree on their columns still produces a rectangular dataset.
44054
44277
  */
44055
44278
  this._writeDatasetFile = async (localPath, batches, delimiter) => {
44056
- const stream = fs9.createWriteStream(localPath);
44279
+ const writer = new BatchWriter_default(fs9.createWriteStream(localPath));
44057
44280
  let header = null;
44058
44281
  let records = 0;
44059
44282
  try {
@@ -44061,25 +44284,19 @@ var init_DeltaShareDriver = __esm({
44061
44284
  for (const record of batch) {
44062
44285
  if (!header) {
44063
44286
  header = Object.keys(record);
44064
- await this._writeLine(stream, CSVParser_default.stringifyRow(header, delimiter));
44287
+ if (!writer.write(CSVParser_default.stringifyRow(header, delimiter)))
44288
+ await writer.waitForDrain();
44065
44289
  }
44066
- await this._writeLine(stream, CSVParser_default.stringifyRow(header.map((column) => this._serializeDatasetValue(record[column])), delimiter));
44290
+ if (!writer.write(CSVParser_default.stringifyRow(header.map((column) => this._serializeDatasetValue(record[column])), delimiter)))
44291
+ await writer.waitForDrain();
44067
44292
  records++;
44068
44293
  }
44069
44294
  }
44070
44295
  } finally {
44071
- await new Promise((resolve) => stream.end(resolve));
44296
+ await writer.close();
44072
44297
  }
44073
44298
  return records;
44074
44299
  };
44075
- // A false from write() means the buffer is full; waiting for the drain just then keeps the queued
44076
- // rows bounded instead of letting them pile up faster than the disk takes them.
44077
- this._writeLine = async (stream, line) => {
44078
- if (stream.write(`${line}
44079
- `))
44080
- return;
44081
- await new Promise((resolve) => stream.once("drain", resolve));
44082
- };
44083
44300
  /**
44084
44301
  * Every file the share reports for this table. `jsonPredicateHints` lets the server skip the
44085
44302
  * files that cannot hold a matching row and `limitHint` the ones past the rows the caller wants;
@@ -44258,18 +44475,20 @@ var init_SqlDatasetWriter = __esm({
44258
44475
  Affirm_default(localPath, "Invalid dataset path");
44259
44476
  Affirm_default.hasItems(columns, "Invalid dataset columns");
44260
44477
  Affirm_default(batches, "Invalid dataset batches");
44261
- const stream = import_fs9.default.createWriteStream(localPath);
44478
+ const writer = new BatchWriter_default(import_fs9.default.createWriteStream(localPath));
44262
44479
  let rows = 0;
44263
44480
  try {
44264
- await this.writeLine(stream, CSVParser_default.stringifyRow(columns, delimiter));
44481
+ if (!writer.write(CSVParser_default.stringifyRow(columns, delimiter)))
44482
+ await writer.waitForDrain();
44265
44483
  for await (const batch of batches) {
44266
44484
  for (const row of batch) {
44267
- await this.writeLine(stream, CSVParser_default.stringifyRow(row.map((value) => this.serialize(value)), delimiter));
44485
+ if (!writer.write(CSVParser_default.stringifyRow(row.map((value) => this.serialize(value)), delimiter)))
44486
+ await writer.waitForDrain();
44268
44487
  rows++;
44269
44488
  }
44270
44489
  }
44271
44490
  } finally {
44272
- await new Promise((resolve) => stream.end(resolve));
44491
+ await writer.close();
44273
44492
  }
44274
44493
  return rows;
44275
44494
  };
@@ -44291,14 +44510,6 @@ var init_SqlDatasetWriter = __esm({
44291
44510
  return value.toString("base64");
44292
44511
  return JSON.stringify(value);
44293
44512
  };
44294
- // A false from write() means the buffer is full; waiting for the drain just then keeps the queued
44295
- // rows bounded instead of letting them pile up faster than the disk takes them.
44296
- this.writeLine = async (stream, line) => {
44297
- if (stream.write(`${line}
44298
- `))
44299
- return;
44300
- await new Promise((resolve) => stream.once("drain", resolve));
44301
- };
44302
44513
  }
44303
44514
  };
44304
44515
  SqlDatasetWriter = new SqlDatasetWriterClass();
@@ -44319,8 +44530,13 @@ var init_SqlQueryBuilder = __esm({
44319
44530
  *
44320
44531
  * Columns are projected in the producer's own order and the caller writes the header from
44321
44532
  * `columnKeys`, so the staged CSV never depends on how a source echoes column names back.
44533
+ *
44534
+ * `options` is what the run asked the source to do for it — a `WHERE` rendered by `SqlPredicate`
44535
+ * and a row limit, both resolved by `SqlPushdown`. Neither changes what the run produces: the
44536
+ * filters are applied per record and the limit enforced regardless, so a statement built without
44537
+ * them reads more rows and returns the same result.
44322
44538
  */
44323
- this.buildProducerSelect = (producer, source, limit) => {
44539
+ this.buildProducerSelect = (producer, source, options2) => {
44324
44540
  Affirm_default(producer, "Invalid producer");
44325
44541
  Affirm_default(source, "Invalid source");
44326
44542
  const { schema, table } = this.resolveTable(producer, source);
@@ -44328,8 +44544,30 @@ var init_SqlQueryBuilder = __esm({
44328
44544
  Affirm_default.hasItems(columns, `Producer "${producer.name}" declares no dimensions, so there is nothing to select from "${table}"`);
44329
44545
  const projection = columns.map((column) => this.quote(column, `column of producer "${producer.name}"`)).join(", ");
44330
44546
  const from = schema ? `${this.quote(schema, `schema of producer "${producer.name}"`)}.${this.quote(table, `table of producer "${producer.name}"`)}` : this.quote(table, `table of producer "${producer.name}"`);
44331
- const select = `SELECT ${projection} FROM ${from}`;
44332
- return limit > 0 ? `${select} LIMIT ${Math.floor(limit)}` : select;
44547
+ const where = options2?.where ? ` WHERE ${options2.where}` : "";
44548
+ const limit = options2?.limit > 0 ? ` LIMIT ${Math.floor(options2.limit)}` : "";
44549
+ return `SELECT ${projection} FROM ${from}${where}${limit}`;
44550
+ };
44551
+ /**
44552
+ * The statement that asks a SQL source what columns a producer's table actually HAS.
44553
+ *
44554
+ * This is what `discover` reads, and it deliberately does not go through `buildProducerSelect`:
44555
+ * that statement is built FROM the producer's dimensions, so sampling it can only ever hand back
44556
+ * the columns the producer already declared — a producer naming one column of a fourteen-column
44557
+ * table discovered exactly that one and was told it had succeeded. The catalog is the only source
44558
+ * of the answer, so the catalog is what is read.
44559
+ *
44560
+ * `information_schema.columns` rather than a dialect catalog (`svv_columns`, `pg_attribute`)
44561
+ * because Redshift and Postgres both expose it with the same column names, which is the same
44562
+ * reason the rest of this builder is dialect-free. One known limit: a Redshift external
44563
+ * (Spectrum) or late-binding-view column is not in `information_schema` and so is not discovered.
44564
+ */
44565
+ this.buildColumnSelect = (producer, source) => {
44566
+ Affirm_default(producer, "Invalid producer");
44567
+ Affirm_default(source, "Invalid source");
44568
+ const { schema, table } = this.resolveTable(producer, source);
44569
+ const schemaMatch = schema ? `table_schema = ${this.nameLiteral(schema, `schema of producer "${producer.name}"`)}` : "table_schema = current_schema()";
44570
+ return `SELECT column_name, data_type, is_nullable, ordinal_position FROM information_schema.columns WHERE ${schemaMatch} AND table_name = ${this.nameLiteral(table, `table of producer "${producer.name}"`)} ORDER BY ordinal_position`;
44333
44571
  };
44334
44572
  /**
44335
44573
  * The source column behind each dimension, in declaration order: `alias` when the dimension
@@ -44345,12 +44583,14 @@ var init_SqlQueryBuilder = __esm({
44345
44583
  * Where the producer's rows live. The table comes from `settings.sqlTable`, optionally qualified
44346
44584
  * as `schema.table`; an unqualified name falls back to the source's own `schema`, and with
44347
44585
  * neither the statement is left unqualified for the connection's `search_path` to resolve.
44586
+ *
44587
+ * A table name is the only thing a producer contributes: there is no hand-written statement to
44588
+ * accept, so every statement sent to a source is one this builder emitted.
44348
44589
  */
44349
44590
  this.resolveTable = (producer, source) => {
44350
44591
  Affirm_default(producer, "Invalid producer");
44351
44592
  Affirm_default(source, "Invalid source");
44352
- const { sqlTable, sql, fileKey } = producer.settings;
44353
- Affirm_default(!sql, `Producer "${producer.name}" sets "sql", which is not supported on a SQL source yet \u2014 use "sqlTable"`);
44593
+ const { sqlTable, fileKey } = producer.settings;
44354
44594
  Affirm_default(!fileKey || sqlTable, `Producer "${producer.name}" sets "fileKey", which a SQL source has no use for \u2014 use "sqlTable"`);
44355
44595
  Affirm_default(sqlTable, `Producer "${producer.name}" reads from SQL source "${source.name}" but declares no "sqlTable"`);
44356
44596
  const parts = sqlTable.split(".");
@@ -44359,6 +44599,12 @@ var init_SqlQueryBuilder = __esm({
44359
44599
  return { schema: parts[0], table: parts[1] };
44360
44600
  return { schema: source.authentication.schema || void 0, table: parts[0] };
44361
44601
  };
44602
+ /**
44603
+ * Whether a name is one `quote` would accept. For a caller that has to answer "can this be put in
44604
+ * a statement at all" without being thrown at — see `SqlPredicate`, where an unusable column is a
44605
+ * filter not pushed rather than a run that fails.
44606
+ */
44607
+ this.isIdentifier = (identifier) => !!identifier && IDENTIFIER_PATTERN2.test(identifier);
44362
44608
  /**
44363
44609
  * One identifier, quoted. The pattern check is the whole defence against a name reaching the
44364
44610
  * server as syntax rather than as a name — a producer's config is not a trusted statement — so
@@ -44369,6 +44615,23 @@ var init_SqlQueryBuilder = __esm({
44369
44615
  Affirm_default(IDENTIFIER_PATTERN2.test(identifier), `Invalid SQL identifier "${identifier}" for ${context}: expected letters, digits, "_" or "$", not starting with a digit`);
44370
44616
  return `"${identifier}"`;
44371
44617
  };
44618
+ /**
44619
+ * The same identifier as a string LITERAL, which is how the catalog stores it: in
44620
+ * `information_schema` a table name is data, not syntax, so it is compared rather than quoted.
44621
+ * It goes through the identical pattern check, which forbids a quote character outright, so
44622
+ * there is nothing here to escape — a name this accepts cannot close the literal.
44623
+ *
44624
+ * The comparison is EXACT, which matches the projection: `buildProducerSelect` quotes every
44625
+ * identifier, so `"Claim"` and `claim` are already two different tables to the server. Postgres
44626
+ * folds an unquoted name to lower case when the table is created, and Redshift folds it always,
44627
+ * so a `sqlTable` that does not match the stored spelling finds nothing — and the caller says so
44628
+ * naming what it looked for, rather than guessing at the case.
44629
+ */
44630
+ this.nameLiteral = (identifier, context) => {
44631
+ Affirm_default(identifier, `Missing SQL identifier for ${context}`);
44632
+ Affirm_default(IDENTIFIER_PATTERN2.test(identifier), `Invalid SQL identifier "${identifier}" for ${context}: expected letters, digits, "_" or "$", not starting with a digit`);
44633
+ return `'${identifier}'`;
44634
+ };
44372
44635
  }
44373
44636
  };
44374
44637
  SqlQueryBuilder = new SqlQueryBuilderClass();
@@ -44376,6 +44639,288 @@ var init_SqlQueryBuilder = __esm({
44376
44639
  }
44377
44640
  });
44378
44641
 
44642
+ // ../../packages/drivers/src/sql/SqlPredicate.ts
44643
+ var ISO_DATE_SHAPE, NUMERIC_LITERAL, isControlCharacter, MAX_LITERAL_CHARS, SqlPredicateClass, SqlPredicate, SqlPredicate_default;
44644
+ var init_SqlPredicate = __esm({
44645
+ "../../packages/drivers/src/sql/SqlPredicate.ts"() {
44646
+ init_SqlQueryBuilder();
44647
+ ISO_DATE_SHAPE = /^\d{4}-\d{2}-\d{2}/;
44648
+ NUMERIC_LITERAL = /^[+-]?(\d+\.?\d*|\.\d+)$/;
44649
+ isControlCharacter = (character) => {
44650
+ const code = character.charCodeAt(0);
44651
+ return code < 32 || code === 127;
44652
+ };
44653
+ MAX_LITERAL_CHARS = 1e3;
44654
+ SqlPredicateClass = class {
44655
+ constructor() {
44656
+ /**
44657
+ * The `WHERE` body for these filters, or `null` when none of them can be expressed.
44658
+ *
44659
+ * `columnTypes` is the type each column has in the SOURCE's own catalog, keyed by column name.
44660
+ * A filter whose column is missing from it, or whose dimension type the catalog disagrees with,
44661
+ * is not pushed: a producer is free to declare a `timestamptz` column as a `string`, and the
44662
+ * statement that compares it as one either prunes the wrong rows or fails outright.
44663
+ */
44664
+ this.build = (filters, columnTypes) => {
44665
+ if (!filters || filters.length === 0)
44666
+ return null;
44667
+ if (!columnTypes || columnTypes.size === 0)
44668
+ return null;
44669
+ return this.all(filters.map((filter) => this.toNode(filter, columnTypes)).filter((x2) => !!x2));
44670
+ };
44671
+ this.toNode = (filter, columnTypes) => {
44672
+ const node = this.toComparison(filter, columnTypes);
44673
+ if (!node)
44674
+ return null;
44675
+ if (filter.and?.length) {
44676
+ const children = filter.and.map((x2) => this.toNode(x2, columnTypes)).filter((x2) => !!x2);
44677
+ return this.all([node, ...children]);
44678
+ }
44679
+ if (filter.or?.length) {
44680
+ const branches = filter.or.map((x2) => this.toNode(x2, columnTypes));
44681
+ return branches.every((x2) => !!x2) ? this.any([node, ...branches]) : null;
44682
+ }
44683
+ return node;
44684
+ };
44685
+ /** The filter's own rule as SQL, ignoring its `and`/`or` children. */
44686
+ this.toComparison = (filter, columnTypes) => {
44687
+ if (columnTypes.get(filter.column) !== filter.type)
44688
+ return null;
44689
+ if (!SqlQueryBuilder_default.isIdentifier(filter.column))
44690
+ return null;
44691
+ const column = SqlQueryBuilder_default.quote(filter.column, `pushdown column "${filter.column}"`);
44692
+ switch (filter.type) {
44693
+ case "string":
44694
+ return this.text(filter, column);
44695
+ case "number":
44696
+ return this.numeric(filter, column);
44697
+ default:
44698
+ return null;
44699
+ }
44700
+ };
44701
+ /**
44702
+ * A text column's rule. The LIKE patterns are built from a value with no backslash in it (see
44703
+ * `isPlainText`), so the only pattern characters a value can carry are `%` and `_` — both of
44704
+ * which match MORE than the literal they stand for, which is the safe direction.
44705
+ */
44706
+ this.text = (filter, column) => {
44707
+ const values = filter.values ?? [];
44708
+ if (values.length === 0 || !values.every(this.isPlainText))
44709
+ return null;
44710
+ switch (filter.operator) {
44711
+ case "equals":
44712
+ return values.length === 1 ? `${column} = ${this.textLiteral(values[0])}` : null;
44713
+ case "in":
44714
+ return `${column} IN (${values.map(this.textLiteral).join(", ")})`;
44715
+ case "contains":
44716
+ return `${column} LIKE ${this.textLiteral(`%${values[0]}%`)}`;
44717
+ case "startsWith":
44718
+ return `${column} LIKE ${this.textLiteral(`${values[0]}%`)}`;
44719
+ case "endsWith":
44720
+ return `${column} LIKE ${this.textLiteral(`%${values[0]}`)}`;
44721
+ default:
44722
+ return null;
44723
+ }
44724
+ };
44725
+ /**
44726
+ * A numeric column's rule. The literal goes over as the digits the filter was given rather than
44727
+ * as the double they parse to, so `1.50` is compared as the server stores it.
44728
+ *
44729
+ * The one shape where the two sides can still disagree is a column holding more precision than a
44730
+ * double: the server compares it exactly and remora compares it rounded, so two values a record
44731
+ * reads as equal are two values to the server. A producer against such a column turns the lot
44732
+ * off with `settings.disablePushdown`.
44733
+ */
44734
+ this.numeric = (filter, column) => {
44735
+ const values = (filter.values ?? []).map(this.numberLiteral);
44736
+ if (values.length === 0 || !values.every((x2) => x2 !== null))
44737
+ return null;
44738
+ switch (filter.operator) {
44739
+ case "equals":
44740
+ return values.length === 1 ? `${column} = ${values[0]}` : null;
44741
+ case "in":
44742
+ return `${column} IN (${values.join(", ")})`;
44743
+ case "greaterThan":
44744
+ return `${column} > ${values[0]}`;
44745
+ case "greaterThanOrEquals":
44746
+ return `${column} >= ${values[0]}`;
44747
+ case "lessThan":
44748
+ return `${column} < ${values[0]}`;
44749
+ case "lessThanOrEquals":
44750
+ return `${column} <= ${values[0]}`;
44751
+ case "between":
44752
+ return values.length === 2 ? `(${column} >= ${values[0]} AND ${column} <= ${values[1]})` : null;
44753
+ default:
44754
+ return null;
44755
+ }
44756
+ };
44757
+ /**
44758
+ * Whether this value can go into a statement as a string literal at all.
44759
+ *
44760
+ * A single quote is doubled, which is how SQL spells one and the only escape both engines read
44761
+ * the same way whatever `standard_conforming_strings` is set to. A BACKSLASH is refused rather
44762
+ * than escaped for that same reason — and because it is the default escape character of `LIKE`,
44763
+ * where it would make the pattern match less than the value it came from.
44764
+ */
44765
+ this.isPlainText = (value) => {
44766
+ const text = String(value ?? "");
44767
+ if (text.includes("\\") || [...text].some(isControlCharacter))
44768
+ return false;
44769
+ if (ISO_DATE_SHAPE.test(text))
44770
+ return false;
44771
+ return text.length <= MAX_LITERAL_CHARS;
44772
+ };
44773
+ this.textLiteral = (value) => `'${String(value).replaceAll("'", "''")}'`;
44774
+ /** The value as a numeric literal, or `null` when it does not name a finite number. */
44775
+ this.numberLiteral = (value) => {
44776
+ const text = String(value ?? "").trim();
44777
+ if (!NUMERIC_LITERAL.test(text) || !Number.isFinite(Number(text)))
44778
+ return null;
44779
+ return text;
44780
+ };
44781
+ /** `null` when there is nothing to join, the node itself when there is one, a group otherwise. */
44782
+ this.all = (nodes5) => this.group("AND", nodes5);
44783
+ this.any = (nodes5) => this.group("OR", nodes5);
44784
+ this.group = (operator, nodes5) => {
44785
+ if (nodes5.length === 0)
44786
+ return null;
44787
+ return nodes5.length === 1 ? nodes5[0] : `(${nodes5.join(` ${operator} `)})`;
44788
+ };
44789
+ }
44790
+ };
44791
+ SqlPredicate = new SqlPredicateClass();
44792
+ SqlPredicate_default = SqlPredicate;
44793
+ }
44794
+ });
44795
+
44796
+ // ../../packages/drivers/src/sql/SqlSchemaReader.ts
44797
+ var NUMERIC_TYPES, TEMPORAL_TYPES, SqlSchemaReaderClass, SqlSchemaReader, SqlSchemaReader_default;
44798
+ var init_SqlSchemaReader = __esm({
44799
+ "../../packages/drivers/src/sql/SqlSchemaReader.ts"() {
44800
+ init_src();
44801
+ init_src2();
44802
+ init_SqlQueryBuilder();
44803
+ NUMERIC_TYPES = /* @__PURE__ */ new Set([
44804
+ "smallint",
44805
+ "integer",
44806
+ "bigint",
44807
+ "decimal",
44808
+ "numeric",
44809
+ "real",
44810
+ "double precision",
44811
+ // Redshift reports these spellings for the same types
44812
+ "int",
44813
+ "int2",
44814
+ "int4",
44815
+ "int8",
44816
+ "float",
44817
+ "float4",
44818
+ "float8"
44819
+ ]);
44820
+ TEMPORAL_TYPES = /* @__PURE__ */ new Set(["date", "timestamp", "timestamptz", "timestamp without time zone", "timestamp with time zone"]);
44821
+ SqlSchemaReaderClass = class {
44822
+ constructor() {
44823
+ /**
44824
+ * Every column of the producer's table, in the table's own order.
44825
+ *
44826
+ * Goes through `driver.query`, which is already capped at `SQL_MAX_QUERY_ROWS` — a table with
44827
+ * more columns than that cap is not a table remora can read anyway. A table that matches nothing
44828
+ * fails here rather than producing a producer with no dimensions, because "this table has no
44829
+ * columns" is never the truth: it is a name that did not resolve.
44830
+ */
44831
+ this.readColumns = async (driver, producer, source) => {
44832
+ Affirm_default(driver, `Invalid source driver for producer "${producer?.name}"`);
44833
+ Affirm_default(producer, "Invalid producer");
44834
+ Affirm_default(source, "Invalid source");
44835
+ const { schema, table } = SqlQueryBuilder_default.resolveTable(producer, source);
44836
+ const select = SqlQueryBuilder_default.buildColumnSelect(producer, source);
44837
+ Logger_default.log(`SQL schema read for producer "${producer.name}" with: ${select}`);
44838
+ const result = await driver.query(select);
44839
+ const rows = result?.rows ?? [];
44840
+ Affirm_default.hasItems(rows, `Table "${schema ? `${schema}.` : ""}${table}" of producer "${producer.name}" has no columns in the catalog of source "${source.name}" \u2014 check the name exists and is spelled with the same case the table was created with, and that this user may read it`);
44841
+ return rows.slice().sort((left, right) => Number(left.ordinal_position) - Number(right.ordinal_position)).map((row) => ({
44842
+ name: String(row.column_name),
44843
+ dataType: String(row.data_type ?? "").trim().toLowerCase(),
44844
+ nullable: String(row.is_nullable ?? "YES").trim().toUpperCase() !== "NO"
44845
+ }));
44846
+ };
44847
+ /**
44848
+ * The dimension type a SQL column reads as. Anything not recognised is a `string`, which is both
44849
+ * the safe answer and the true one for the types that reach a staged CSV as text anyway — `json`,
44850
+ * `uuid`, an array, a Redshift `super`.
44851
+ */
44852
+ this.toDimensionType = (dataType) => {
44853
+ const type = (dataType ?? "").trim().toLowerCase();
44854
+ if (NUMERIC_TYPES.has(type)) return "number";
44855
+ if (type === "boolean" || type === "bool") return "boolean";
44856
+ if (TEMPORAL_TYPES.has(type)) return "datetime";
44857
+ return "string";
44858
+ };
44859
+ }
44860
+ };
44861
+ SqlSchemaReader = new SqlSchemaReaderClass();
44862
+ SqlSchemaReader_default = SqlSchemaReader;
44863
+ }
44864
+ });
44865
+
44866
+ // ../../packages/drivers/src/sql/SqlPushdown.ts
44867
+ var SqlPushdownClass, SqlPushdown, SqlPushdown_default;
44868
+ var init_SqlPushdown = __esm({
44869
+ "../../packages/drivers/src/sql/SqlPushdown.ts"() {
44870
+ init_src();
44871
+ init_src2();
44872
+ init_SqlPredicate();
44873
+ init_SqlSchemaReader();
44874
+ SqlPushdownClass = class {
44875
+ constructor() {
44876
+ /** What this producer's staging statement should carry. Empty when there is nothing to push. */
44877
+ this.resolve = async (request) => {
44878
+ Affirm_default(request, "Invalid SQL pushdown request");
44879
+ const { driver, producer, source, plan } = request;
44880
+ Affirm_default(driver, "Invalid source driver");
44881
+ Affirm_default(producer, "Invalid producer");
44882
+ Affirm_default(source, "Invalid source");
44883
+ if (!plan || producer.settings?.disablePushdown)
44884
+ return {};
44885
+ return {
44886
+ where: await this.buildWhere(driver, producer, source, plan),
44887
+ limit: plan.limit > 0 ? plan.limit : void 0
44888
+ };
44889
+ };
44890
+ this.buildWhere = async (driver, producer, source, plan) => {
44891
+ const filters = plan.filters ?? [];
44892
+ if (filters.length === 0)
44893
+ return null;
44894
+ const columnTypes = await this.readColumnTypes(driver, producer, source);
44895
+ const where = SqlPredicate_default.build(filters, columnTypes);
44896
+ if (!where && columnTypes)
44897
+ Logger_default.log(`SQL pushdown: none of the ${filters.length} filter(s) of producer "${producer.name}" can be expressed as a WHERE clause`);
44898
+ return where;
44899
+ };
44900
+ /**
44901
+ * The type the SOURCE gives each of the table's columns, keyed by column name, or `null` when
44902
+ * the catalog cannot be read.
44903
+ *
44904
+ * A table that is not in `information_schema` — a Redshift external (Spectrum) table, a
44905
+ * late-binding view — is a table nothing is pushed to rather than a run that fails: the staging
44906
+ * read itself does not go through the catalog and is unaffected.
44907
+ */
44908
+ this.readColumnTypes = async (driver, producer, source) => {
44909
+ try {
44910
+ const columns = await SqlSchemaReader_default.readColumns(driver, producer, source);
44911
+ return new Map(columns.map((column) => [column.name, SqlSchemaReader_default.toDimensionType(column.dataType)]));
44912
+ } catch (error) {
44913
+ Logger_default.warn(`SQL pushdown: the columns of producer "${producer.name}" could not be read from the catalog of source "${source.name}", so its filters are applied per record instead: ${error}`);
44914
+ return null;
44915
+ }
44916
+ };
44917
+ }
44918
+ };
44919
+ SqlPushdown = new SqlPushdownClass();
44920
+ SqlPushdown_default = SqlPushdown;
44921
+ }
44922
+ });
44923
+
44379
44924
  // ../../packages/drivers/src/RedshiftDriver.ts
44380
44925
  var import_fs10, import_client_redshift_data, RedshiftDriver, RedshiftDriver_default;
44381
44926
  var init_RedshiftDriver = __esm({
@@ -44389,6 +44934,7 @@ var init_RedshiftDriver = __esm({
44389
44934
  init_src2();
44390
44935
  init_AwsCredentials();
44391
44936
  init_SqlDatasetWriter();
44937
+ init_SqlPushdown();
44392
44938
  init_SqlQueryBuilder();
44393
44939
  RedshiftDriver = class {
44394
44940
  constructor() {
@@ -44403,7 +44949,11 @@ var init_RedshiftDriver = __esm({
44403
44949
  Affirm_default(this._dbName, `Source "${source.name}" is missing authentication.database`);
44404
44950
  Affirm_default(this._workgroup || this._clusterId, `Source "${source.name}" is missing authentication.workgroup (Redshift Serverless) or authentication.clusterId (a provisioned cluster)`);
44405
44951
  const config = {
44406
- region: source.authentication["region"],
44952
+ // Through replaceSecret like every other field: `region` is as much a `{ENV_VAR}`
44953
+ // candidate as the database name, and the raw reference reached the SDK as a literal
44954
+ // and failed as `region="{REDSHIFT_REGION}" is not a valid hostname component` — an
44955
+ // error naming neither the source nor the variable that was not resolved.
44956
+ region: SecretManager_default.replaceSecret(source.authentication["region"]),
44407
44957
  credentials: resolveAwsCredentials(source.authentication)
44408
44958
  };
44409
44959
  try {
@@ -44441,7 +44991,8 @@ var init_RedshiftDriver = __esm({
44441
44991
  Affirm_default(producer, "Invalid producer");
44442
44992
  Affirm_default(scope, "Invalid executor scope");
44443
44993
  const { table } = SqlQueryBuilder_default.resolveTable(producer, this._source);
44444
- const select = SqlQueryBuilder_default.buildProducerSelect(producer, this._source);
44994
+ const pushdown = await SqlPushdown_default.resolve({ driver: this, producer, source: this._source, plan: request.pushdown });
44995
+ const select = SqlQueryBuilder_default.buildProducerSelect(producer, this._source, pushdown);
44445
44996
  const columns = SqlQueryBuilder_default.columnKeys(producer);
44446
44997
  const localPath = ExecutorScope_default.getProducerPath(scope, producer, table);
44447
44998
  ExecutorScope_default.ensurePath(localPath);
@@ -44454,7 +45005,7 @@ var init_RedshiftDriver = __esm({
44454
45005
  const reported = statement.ResultRows >= 0 ? statement.ResultRows : null;
44455
45006
  Affirm_default(reported === null || reported === rows, `Redshift ready: staged ${rows} row(s) for producer "${producer.name}" but the statement reported ${reported}`);
44456
45007
  Logger_default.log(`Redshift ready: staged ${rows} row(s) (${fileSize} bytes) for producer "${producer.name}" from table "${table}"`);
44457
- return { files: [{ fullUri: localPath, fileSize }] };
45008
+ return { files: [{ fullUri: localPath, fileSize }], filteredAtSource: !!pushdown.where };
44458
45009
  };
44459
45010
  this.execute = async (sql) => {
44460
45011
  Affirm_default(sql, `Invalid SQL`);
@@ -44669,6 +45220,7 @@ var init_PostgresDriver = __esm({
44669
45220
  init_src2();
44670
45221
  init_AwsCredentials();
44671
45222
  init_SqlDatasetWriter();
45223
+ init_SqlPushdown();
44672
45224
  init_SqlQueryBuilder();
44673
45225
  PostgresDriver = class {
44674
45226
  constructor() {
@@ -44698,7 +45250,8 @@ var init_PostgresDriver = __esm({
44698
45250
  Affirm_default(producer, "Invalid producer");
44699
45251
  Affirm_default(scope, "Invalid executor scope");
44700
45252
  const { table } = SqlQueryBuilder_default.resolveTable(producer, this._source);
44701
- const select = SqlQueryBuilder_default.buildProducerSelect(producer, this._source);
45253
+ const pushdown = await SqlPushdown_default.resolve({ driver: this, producer, source: this._source, plan: request.pushdown });
45254
+ const select = SqlQueryBuilder_default.buildProducerSelect(producer, this._source, pushdown);
44702
45255
  const columns = SqlQueryBuilder_default.columnKeys(producer);
44703
45256
  const localPath = ExecutorScope_default.getProducerPath(scope, producer, table);
44704
45257
  ExecutorScope_default.ensurePath(localPath);
@@ -44714,7 +45267,7 @@ var init_PostgresDriver = __esm({
44714
45267
  onProgress?.(1);
44715
45268
  const fileSize = import_fs11.default.statSync(localPath).size;
44716
45269
  Logger_default.log(`Postgres ready: staged ${rows} row(s) (${fileSize} bytes) for producer "${producer.name}" from table "${table}"`);
44717
- return { files: [{ fullUri: localPath, fileSize }] };
45270
+ return { files: [{ fullUri: localPath, fileSize }], filteredAtSource: !!pushdown.where };
44718
45271
  };
44719
45272
  /**
44720
45273
  * A bounded, ad-hoc read — the sample and discover paths, never a run. Capped at
@@ -44789,10 +45342,27 @@ var init_PostgresDriver = __esm({
44789
45342
  try {
44790
45343
  await client.connect();
44791
45344
  } catch (error) {
44792
- throw new Error(`Unable to connect to ${this.describeTarget()} for source "${this._source.name}": ${error.message}`);
45345
+ throw new Error(`Unable to connect to ${this.describeTarget()} for source "${this._source.name}": ${this.describeConnectionError(error)}`);
44793
45346
  }
44794
45347
  return client;
44795
45348
  };
45349
+ /**
45350
+ * Why a connection failed, in words. A refused or timed-out connection is the commonest thing to
45351
+ * go wrong with a SQL source and the one case where `error.message` is EMPTY: node resolves
45352
+ * localhost to both ::1 and 127.0.0.1, and when every address fails `pg` rejects with an
45353
+ * `AggregateError` whose message is blank and whose reasons are in `errors`. Reporting that
45354
+ * verbatim produced "Unable to connect to postgres://host:5432/db for source "x": " — the
45355
+ * message naming everything except the thing the operator needs. So the reason is taken from
45356
+ * whichever of the three carries one, and the address list is deduplicated because "refused on
45357
+ * ::1 and on 127.0.0.1" is one fact about one host, not two.
45358
+ */
45359
+ this.describeConnectionError = (error) => {
45360
+ const failure = error;
45361
+ if (failure?.message) return failure.message;
45362
+ const reasons = [...new Set((failure?.errors ?? []).map((reason) => reason?.message).filter(Boolean))];
45363
+ if (reasons.length > 0) return `${failure.code ? `${failure.code}: ` : ""}${reasons.join("; ")}`;
45364
+ return failure?.code || "no reason reported by the driver";
45365
+ };
44796
45366
  this.buildConfig = async () => {
44797
45367
  const host = SecretManager_default.replaceSecret(this._auth.host);
44798
45368
  const port = parseInt(SecretManager_default.replaceSecret(this._auth.port) || "5432");
@@ -45055,11 +45625,10 @@ var init_HttpApiDriver = __esm({
45055
45625
  });
45056
45626
 
45057
45627
  // ../../packages/drivers/src/DriverHelper.ts
45058
- var import_stream2, import_readline, import_promises9, import_fs12, DriverHelper, DriverHelper_default;
45628
+ var import_stream2, import_promises9, import_fs12, DriverHelper, DriverHelper_default;
45059
45629
  var init_DriverHelper = __esm({
45060
45630
  "../../packages/drivers/src/DriverHelper.ts"() {
45061
45631
  import_stream2 = require("stream");
45062
- import_readline = require("readline");
45063
45632
  import_promises9 = require("stream/promises");
45064
45633
  import_fs12 = require("fs");
45065
45634
  init_src2();
@@ -45160,51 +45729,40 @@ var init_DriverHelper = __esm({
45160
45729
  Affirm_default(options2, "Invalid options");
45161
45730
  const { append, destinationPath, objects, delimiter } = options2;
45162
45731
  const writeOptions = append ? { flags: "a" } : {};
45163
- const writeStream = (0, import_fs12.createWriteStream)(destinationPath, writeOptions);
45164
- const waitForDrain = () => new Promise((resolve) => writeStream.once("drain", resolve));
45732
+ const writer = new BatchWriter_default((0, import_fs12.createWriteStream)(destinationPath, writeOptions));
45165
45733
  let lineCount = 0;
45166
45734
  const keys = Object.keys(objects[0]);
45167
45735
  for (const obj of objects) {
45168
- const serialized = keys.map((k7) => obj[k7]).join(delimiter) + "\n";
45169
- if (!writeStream.write(serialized))
45170
- await waitForDrain();
45736
+ if (!writer.write(keys.map((k7) => obj[k7]).join(delimiter)))
45737
+ await writer.waitForDrain();
45171
45738
  lineCount++;
45172
45739
  }
45173
- writeStream.end();
45174
- await new Promise((resolve, reject) => {
45175
- writeStream.on("finish", resolve);
45176
- writeStream.on("error", reject);
45177
- });
45740
+ await writer.close();
45178
45741
  return lineCount;
45179
45742
  },
45180
45743
  quickReadFile: async (filePath, lineCount) => {
45181
45744
  const fileStream = (0, import_fs12.createReadStream)(filePath);
45182
- const rl8 = (0, import_readline.createInterface)({ input: fileStream, crlfDelay: Infinity });
45183
45745
  const lines = [];
45184
45746
  let counter = 0;
45185
- for await (const line of rl8) {
45747
+ for await (const line of readLines(fileStream)) {
45186
45748
  lines.push(line);
45187
45749
  counter++;
45188
45750
  if (counter >= lineCount) {
45189
45751
  break;
45190
45752
  }
45191
45753
  }
45192
- rl8.close();
45193
- fileStream.close();
45194
45754
  return lines;
45195
45755
  },
45196
45756
  quickReadStream: async (stream, lineCount) => {
45197
- const rl8 = (0, import_readline.createInterface)({ input: stream, crlfDelay: Infinity });
45198
45757
  const lines = [];
45199
45758
  let counter = 0;
45200
- for await (const line of rl8) {
45759
+ for await (const line of readLines(stream)) {
45201
45760
  lines.push(line);
45202
45761
  counter++;
45203
45762
  if (counter >= lineCount) {
45204
45763
  break;
45205
45764
  }
45206
45765
  }
45207
- rl8.close();
45208
45766
  return lines;
45209
45767
  }
45210
45768
  };
@@ -45213,12 +45771,11 @@ var init_DriverHelper = __esm({
45213
45771
  });
45214
45772
 
45215
45773
  // ../../packages/drivers/src/files/LocalSourceDriver.ts
45216
- var fs13, import_path9, import_readline2, XLSX2, LocalSourceDriver;
45774
+ var fs13, import_path9, XLSX2, LocalSourceDriver;
45217
45775
  var init_LocalSourceDriver = __esm({
45218
45776
  "../../packages/drivers/src/files/LocalSourceDriver.ts"() {
45219
45777
  fs13 = __toESM(require("fs"), 1);
45220
45778
  import_path9 = __toESM(require("path"), 1);
45221
- import_readline2 = __toESM(require("readline"), 1);
45222
45779
  init_src();
45223
45780
  init_src();
45224
45781
  XLSX2 = __toESM(require("@e965/xlsx"), 1);
@@ -45293,10 +45850,9 @@ var init_LocalSourceDriver = __esm({
45293
45850
  };
45294
45851
  this._readLines = async (fileUri, lineFrom, lineTo) => {
45295
45852
  const stream = fs13.createReadStream(fileUri);
45296
- const reader = import_readline2.default.createInterface({ input: stream, crlfDelay: Infinity });
45297
45853
  const lines = [];
45298
45854
  let lineCounter = 0;
45299
- for await (const line of reader) {
45855
+ for await (const line of readLines(stream)) {
45300
45856
  if (Algo_default.hasVal(lineFrom) && Algo_default.hasVal(lineTo)) {
45301
45857
  if (lineCounter >= lineFrom && lineCounter < lineTo) {
45302
45858
  if (line && line.length > 0)
@@ -45309,8 +45865,6 @@ var init_LocalSourceDriver = __esm({
45309
45865
  lines.push(line);
45310
45866
  }
45311
45867
  }
45312
- reader.close();
45313
- stream.close();
45314
45868
  return lines;
45315
45869
  };
45316
45870
  this._readExcelLines = async (fileUri, sheetName, lineFrom, lineTo, startRow, startColumn) => {
@@ -45456,12 +46010,12 @@ var init_LocalSourceDriver = __esm({
45456
46010
  });
45457
46011
 
45458
46012
  // ../../packages/drivers/src/files/LocalDestinationDriver.ts
45459
- var fs14, import_promises10, import_readline3, import_path10, LocalDestinationDriver;
46013
+ var fs14, import_crypto4, import_promises10, import_path10, LocalDestinationDriver;
45460
46014
  var init_LocalDestinationDriver = __esm({
45461
46015
  "../../packages/drivers/src/files/LocalDestinationDriver.ts"() {
45462
46016
  fs14 = __toESM(require("fs"), 1);
46017
+ import_crypto4 = require("crypto");
45463
46018
  import_promises10 = __toESM(require("fs/promises"), 1);
45464
- import_readline3 = __toESM(require("readline"), 1);
45465
46019
  import_path10 = __toESM(require("path"), 1);
45466
46020
  init_src();
45467
46021
  init_src2();
@@ -45492,37 +46046,57 @@ var init_LocalDestinationDriver = __esm({
45492
46046
  const fileContent = await s3Driver.downloadFile(sourceFileKey);
45493
46047
  await this.saveFile(destinationFileKey, fileContent);
45494
46048
  };
46049
+ /**
46050
+ * A private path to build the export at, beside the file it will become.
46051
+ *
46052
+ * Two runs of one consumer share an export name — that is what a configured `exportName` means —
46053
+ * so they target the same path, and a run started by CRON while an API call is still exporting
46054
+ * would otherwise have both streams writing into one file at their own offsets. The result is a
46055
+ * file holding neither run: interleaved lines, and a torn line where they met.
46056
+ *
46057
+ * Building under a unique name and renaming into place makes the swap atomic, so concurrent runs
46058
+ * degrade to a clean last-writer-wins — one run's output, whole — instead of corruption. A
46059
+ * sibling of the destination on purpose: rename is only atomic within one filesystem.
46060
+ */
46061
+ this._stagingPath = (toFilePath) => `${toFilePath}.${(0, import_crypto4.randomUUID)()}.partial`;
46062
+ /** Removes a staging file a failed export left behind; never masks the original error. */
46063
+ this._discardStaging = async (stagingPath) => {
46064
+ try {
46065
+ await import_promises10.default.unlink(stagingPath);
46066
+ } catch {
46067
+ }
46068
+ };
45495
46069
  this.move = async (fromPath, toName) => {
46070
+ const toFilePath = import_path10.default.join(this._path, toName);
46071
+ const stagingPath = this._stagingPath(toFilePath);
45496
46072
  try {
45497
- const toFilePath = import_path10.default.join(this._path, toName);
45498
- await import_promises10.default.copyFile(fromPath, toFilePath);
46073
+ await import_promises10.default.copyFile(fromPath, stagingPath);
46074
+ await import_promises10.default.rename(stagingPath, toFilePath);
45499
46075
  await import_promises10.default.unlink(fromPath);
45500
46076
  return { bucket: "", key: toFilePath, res: true };
45501
46077
  } catch (error) {
45502
46078
  Logger_default.error(error);
46079
+ await this._discardStaging(stagingPath);
45503
46080
  return { bucket: "", key: "", res: false };
45504
46081
  }
45505
46082
  };
45506
46083
  this.transformAndMove = async (fromPath, transform, toName) => {
46084
+ const toFilePath = import_path10.default.join(this._path, toName);
46085
+ const stagingPath = this._stagingPath(toFilePath);
45507
46086
  try {
45508
- const toFilePath = import_path10.default.join(this._path, toName);
45509
46087
  const reader = fs14.createReadStream(fromPath);
45510
- const lineReader = import_readline3.default.createInterface({ input: reader, crlfDelay: Infinity });
45511
- const writer = fs14.createWriteStream(toFilePath);
45512
- const waitForDrain = () => new Promise((resolve) => writer.once("drain", resolve));
45513
- for await (const line of lineReader) {
45514
- if (!writer.write(transform(line) + "\n"))
45515
- await waitForDrain();
46088
+ const writer = new BatchWriter_default(fs14.createWriteStream(stagingPath));
46089
+ for await (const line of readLines(reader)) {
46090
+ if (!writer.write(transform(line)))
46091
+ await writer.waitForDrain();
45516
46092
  }
45517
- writer.end();
45518
- await new Promise((resolve, reject) => {
45519
- writer.on("finish", resolve);
45520
- writer.on("error", reject);
45521
- });
46093
+ await writer.close();
45522
46094
  reader.close();
46095
+ await import_promises10.default.rename(stagingPath, toFilePath);
45523
46096
  return { bucket: "", key: toFilePath, res: true };
45524
46097
  } catch (error) {
45525
46098
  Logger_default.error(error);
46099
+ await this._discardStaging(stagingPath);
45526
46100
  return { bucket: "", key: "", res: false };
45527
46101
  }
45528
46102
  };
@@ -45531,89 +46105,24 @@ var init_LocalDestinationDriver = __esm({
45531
46105
  }
45532
46106
  });
45533
46107
 
45534
- // ../../packages/drivers/src/s3/S3SourceDriver.ts
45535
- var import_client_s32, import_stream3, import_promises11, import_readline4, import_path11, import_fs13, import_xlsx2, S3SourceDriver;
45536
- var init_S3SourceDriver = __esm({
45537
- "../../packages/drivers/src/s3/S3SourceDriver.ts"() {
45538
- import_client_s32 = require("@aws-sdk/client-s3");
46108
+ // ../../packages/drivers/src/StreamReader.ts
46109
+ var import_xlsx2, StreamReaderClass, StreamReader, StreamReader_default;
46110
+ var init_StreamReader = __esm({
46111
+ "../../packages/drivers/src/StreamReader.ts"() {
45539
46112
  init_src();
45540
- init_AwsCredentials();
45541
- import_stream3 = require("stream");
45542
- import_promises11 = require("stream/promises");
45543
- import_readline4 = __toESM(require("readline"), 1);
45544
- import_path11 = __toESM(require("path"), 1);
45545
- import_fs13 = __toESM(require("fs"), 1);
45546
- init_src();
45547
- import_xlsx2 = __toESM(require("@e965/xlsx"), 1);
45548
46113
  init_src5();
45549
- init_src6();
45550
- init_src5();
45551
- init_src4();
45552
- S3SourceDriver = class {
46114
+ import_xlsx2 = __toESM(require("@e965/xlsx"), 1);
46115
+ StreamReaderClass = class {
45553
46116
  constructor() {
45554
- this.init = async (source) => {
45555
- this._bucketName = source.authentication["bucket"];
45556
- const config = {
45557
- region: source.authentication["region"],
45558
- credentials: resolveAwsCredentials(source.authentication)
45559
- };
45560
- this._client = new import_client_s32.S3Client(config);
45561
- return this;
45562
- };
45563
- this.readAll = async (request) => {
45564
- Affirm_default(this._client, 'S3 client not yet initialized, call "connect()" first');
45565
- Affirm_default(request, `Invalid download request`);
45566
- Affirm_default(request.fileKey, `Invalid file key for download request`);
45567
- const { fileKey } = request;
45568
- if (fileKey.includes("%")) {
45569
- const allFileKeys = await this.listFiles(fileKey);
45570
- Affirm_default(allFileKeys.length < 50, `Pattern ${fileKey} of producer requested to S3 matches more than 50 files (${allFileKeys.length}), this is more than the S3 allowed limit. Please refine your pattern, remove some files or use a separate bucket.`);
45571
- const promises2 = allFileKeys.map((x2, i6) => this._get({ ...request, fileKey: x2 }, i6));
45572
- const results = await Promise.all(promises2);
45573
- return results.flat();
45574
- } else {
45575
- return await this._get(request);
45576
- }
45577
- };
45578
- this.readLinesInRange = async (request) => {
45579
- Affirm_default(this._client, 'S3 client not yet initialized, call "connect()" first');
45580
- Affirm_default(request, "Invalid read request");
45581
- Affirm_default(request.options, "Invalid read request options");
45582
- const { fileKey } = request;
45583
- if (fileKey.includes("%")) {
45584
- const allFileKeys = await this.listFiles(fileKey);
45585
- const promises2 = allFileKeys.map((x2, i6) => this._get({ ...request, fileKey: x2 }, i6));
45586
- const results = await Promise.all(promises2);
45587
- return results.flat();
45588
- } else {
45589
- return await this._get(request);
45590
- }
45591
- };
45592
- this.exist = async (producer) => {
45593
- Affirm_default(this._client, 'S3 client not yet initialized, call "connect()" first');
45594
- Affirm_default(producer, "Invalid read producer");
45595
- const bucket = this._bucketName;
45596
- const fileKey = producer.settings.fileKey;
45597
- Affirm_default(fileKey, `Invalid file key for download request`);
45598
- if (fileKey.includes("%")) {
45599
- const allFileKeys = await this.listFiles(fileKey);
45600
- return allFileKeys.length > 0;
45601
- } else {
45602
- try {
45603
- await this._client.send(new import_client_s32.HeadObjectCommand({ Bucket: bucket, Key: fileKey }));
45604
- return true;
45605
- } catch (error) {
45606
- if (error.$metadata?.httpStatusCode === 404 || error.name === "NotFound")
45607
- return false;
45608
- throw error;
45609
- }
45610
- }
45611
- };
45612
- this._readLines = async (stream, lineFrom, lineTo) => {
45613
- const reader = import_readline4.default.createInterface({ input: stream, crlfDelay: Infinity });
46117
+ /**
46118
+ * Reads the stream as lines, optionally keeping only `[lineFrom, lineTo)`.
46119
+ * Empty lines are dropped, and the read stops as soon as the range is satisfied.
46120
+ */
46121
+ this.readLines = async (stream, lineFrom, lineTo) => {
46122
+ Affirm_default(stream, "Invalid stream");
45614
46123
  const lines = [];
45615
46124
  let lineCounter = 0;
45616
- for await (const line of reader) {
46125
+ for await (const line of readLines(stream)) {
45617
46126
  if (Algo_default.hasVal(lineFrom) && Algo_default.hasVal(lineTo)) {
45618
46127
  if (lineCounter >= lineFrom && lineCounter < lineTo) {
45619
46128
  if (line && line.length > 0)
@@ -45626,10 +46135,11 @@ var init_S3SourceDriver = __esm({
45626
46135
  lines.push(line);
45627
46136
  }
45628
46137
  }
45629
- reader.close();
45630
46138
  return lines;
45631
46139
  };
45632
- this._readExcelLines = async (stream, sheetName, lineFrom, lineTo, startRow, startColumn) => {
46140
+ /** Reads one sheet of a workbook as CSV lines. The whole workbook is buffered — XLSX has no streaming decode. */
46141
+ this.readExcelLines = async (stream, sheetName, lineFrom, lineTo, startRow, startColumn) => {
46142
+ Affirm_default(stream, "Invalid stream");
45633
46143
  Affirm_default(sheetName, `Invalid sheetname`);
45634
46144
  const chunks = [];
45635
46145
  for await (const chunk of stream) {
@@ -45652,7 +46162,9 @@ var init_S3SourceDriver = __esm({
45652
46162
  else
45653
46163
  return lines;
45654
46164
  };
45655
- this._readXmlLines = async (stream, lineFrom, lineTo) => {
46165
+ /** Reads the stream as XML and flattens it to one JSON line per record. Buffered, like the Excel path. */
46166
+ this.readXmlLines = async (stream, lineFrom, lineTo) => {
46167
+ Affirm_default(stream, "Invalid stream");
45656
46168
  const chunks = [];
45657
46169
  for await (const chunk of stream) {
45658
46170
  chunks.push(chunk);
@@ -45665,24 +46177,19 @@ var init_S3SourceDriver = __esm({
45665
46177
  }
45666
46178
  return lines;
45667
46179
  };
45668
- this._get = async (request, index) => {
45669
- const { fileKey, fileType, options: options2 } = request;
45670
- const bucket = this._bucketName;
45671
- let lineFrom, lineTo, sheetName, hasHeaderRow, startRow, startColumn;
45672
- if (options2) {
45673
- lineFrom = options2.lineFrom;
45674
- lineTo = options2.lineTo;
45675
- sheetName = options2.sheetName;
45676
- hasHeaderRow = options2.hasHeaderRow;
45677
- startRow = options2.startRow;
45678
- startColumn = options2.startColumn;
45679
- }
45680
- const response = await this._client.send(new import_client_s32.GetObjectCommand({
45681
- Bucket: bucket,
45682
- Key: fileKey
45683
- }));
45684
- Affirm_default(response.Body, "Failed to fetch object from S3");
45685
- const stream = response.Body;
46180
+ /**
46181
+ * Reads a stream as the lines of `request.fileType`, honouring any line range in `request.options`.
46182
+ *
46183
+ * `index` is this file's position within a pattern match: anything but the first file drops its
46184
+ * header row, or the header would repeat in the middle of the unified dataset.
46185
+ */
46186
+ this.read = async (stream, request, index) => {
46187
+ Affirm_default(stream, "Invalid stream");
46188
+ Affirm_default(request, "Invalid read request");
46189
+ Affirm_default(request.fileType, "Invalid file type for read request");
46190
+ const { fileType, options: options2 } = request;
46191
+ const { lineFrom, lineTo, sheetName, hasHeaderRow, startRow, startColumn } = options2 ?? {};
46192
+ const hasRange = Algo_default.hasVal(lineFrom) && Algo_default.hasVal(lineTo);
45686
46193
  let lines = [];
45687
46194
  switch (fileType) {
45688
46195
  case "CSV":
@@ -45690,30 +46197,110 @@ var init_S3SourceDriver = __esm({
45690
46197
  case "JSONL":
45691
46198
  case "TXT":
45692
46199
  case "FIXED":
45693
- if (Algo_default.hasVal(lineFrom) && Algo_default.hasVal(lineTo))
45694
- lines = await this._readLines(stream, lineFrom, lineTo);
45695
- else
45696
- lines = await this._readLines(stream);
46200
+ lines = hasRange ? await this.readLines(stream, lineFrom, lineTo) : await this.readLines(stream);
45697
46201
  break;
45698
46202
  case "XLS":
45699
46203
  case "XLSX":
45700
- if (Algo_default.hasVal(lineFrom) && Algo_default.hasVal(lineTo))
45701
- lines = await this._readExcelLines(stream, sheetName, lineFrom, lineTo, startRow, startColumn);
45702
- else
45703
- lines = await this._readExcelLines(stream, sheetName, void 0, void 0, startRow, startColumn);
46204
+ lines = hasRange ? await this.readExcelLines(stream, sheetName, lineFrom, lineTo, startRow, startColumn) : await this.readExcelLines(stream, sheetName, void 0, void 0, startRow, startColumn);
45704
46205
  break;
45705
46206
  case "XML":
45706
- if (Algo_default.hasVal(lineFrom) && Algo_default.hasVal(lineTo))
45707
- lines = await this._readXmlLines(stream, lineFrom, lineTo);
45708
- else
45709
- lines = await this._readXmlLines(stream);
46207
+ lines = hasRange ? await this.readXmlLines(stream, lineFrom, lineTo) : await this.readXmlLines(stream);
45710
46208
  break;
45711
46209
  }
45712
- if (index > 0 && ParseHelper_default.shouldHaveHeader(fileType, hasHeaderRow)) {
46210
+ if (index > 0 && ParseHelper_default.shouldHaveHeader(fileType, hasHeaderRow))
45713
46211
  lines = lines.slice(1);
45714
- }
45715
46212
  return lines;
45716
46213
  };
46214
+ }
46215
+ };
46216
+ StreamReader = new StreamReaderClass();
46217
+ StreamReader_default = StreamReader;
46218
+ }
46219
+ });
46220
+
46221
+ // ../../packages/drivers/src/s3/S3SourceDriver.ts
46222
+ var import_client_s32, import_stream3, import_promises11, import_path11, import_fs13, S3SourceDriver;
46223
+ var init_S3SourceDriver = __esm({
46224
+ "../../packages/drivers/src/s3/S3SourceDriver.ts"() {
46225
+ import_client_s32 = require("@aws-sdk/client-s3");
46226
+ init_src();
46227
+ init_AwsCredentials();
46228
+ import_stream3 = require("stream");
46229
+ import_promises11 = require("stream/promises");
46230
+ import_path11 = __toESM(require("path"), 1);
46231
+ import_fs13 = __toESM(require("fs"), 1);
46232
+ init_src6();
46233
+ init_StreamReader();
46234
+ init_src4();
46235
+ S3SourceDriver = class {
46236
+ constructor() {
46237
+ this.init = async (source) => {
46238
+ this._bucketName = SecretManager_default.replaceSecret(source.authentication["bucket"]);
46239
+ const config = {
46240
+ region: SecretManager_default.replaceSecret(source.authentication["region"]),
46241
+ credentials: resolveAwsCredentials(source.authentication)
46242
+ };
46243
+ this._client = new import_client_s32.S3Client(config);
46244
+ return this;
46245
+ };
46246
+ this.readAll = async (request) => {
46247
+ Affirm_default(this._client, 'S3 client not yet initialized, call "connect()" first');
46248
+ Affirm_default(request, `Invalid download request`);
46249
+ Affirm_default(request.fileKey, `Invalid file key for download request`);
46250
+ const { fileKey } = request;
46251
+ if (fileKey.includes("%")) {
46252
+ const allFileKeys = await this.listFiles(fileKey);
46253
+ Affirm_default(allFileKeys.length < 50, `Pattern ${fileKey} of producer requested to S3 matches more than 50 files (${allFileKeys.length}), this is more than the S3 allowed limit. Please refine your pattern, remove some files or use a separate bucket.`);
46254
+ const promises2 = allFileKeys.map((x2, i6) => this._get({ ...request, fileKey: x2 }, i6));
46255
+ const results = await Promise.all(promises2);
46256
+ return results.flat();
46257
+ } else {
46258
+ return await this._get(request);
46259
+ }
46260
+ };
46261
+ this.readLinesInRange = async (request) => {
46262
+ Affirm_default(this._client, 'S3 client not yet initialized, call "connect()" first');
46263
+ Affirm_default(request, "Invalid read request");
46264
+ Affirm_default(request.options, "Invalid read request options");
46265
+ const { fileKey } = request;
46266
+ if (fileKey.includes("%")) {
46267
+ const allFileKeys = await this.listFiles(fileKey);
46268
+ const promises2 = allFileKeys.map((x2, i6) => this._get({ ...request, fileKey: x2 }, i6));
46269
+ const results = await Promise.all(promises2);
46270
+ return results.flat();
46271
+ } else {
46272
+ return await this._get(request);
46273
+ }
46274
+ };
46275
+ this.exist = async (producer) => {
46276
+ Affirm_default(this._client, 'S3 client not yet initialized, call "connect()" first');
46277
+ Affirm_default(producer, "Invalid read producer");
46278
+ const bucket = this._bucketName;
46279
+ const fileKey = producer.settings.fileKey;
46280
+ Affirm_default(fileKey, `Invalid file key for download request`);
46281
+ if (fileKey.includes("%")) {
46282
+ const allFileKeys = await this.listFiles(fileKey);
46283
+ return allFileKeys.length > 0;
46284
+ } else {
46285
+ try {
46286
+ await this._client.send(new import_client_s32.HeadObjectCommand({ Bucket: bucket, Key: fileKey }));
46287
+ return true;
46288
+ } catch (error) {
46289
+ if (error.$metadata?.httpStatusCode === 404 || error.name === "NotFound")
46290
+ return false;
46291
+ throw error;
46292
+ }
46293
+ }
46294
+ };
46295
+ this._get = async (request, index) => {
46296
+ const { fileKey } = request;
46297
+ const response = await this._client.send(new import_client_s32.GetObjectCommand({
46298
+ Bucket: this._bucketName,
46299
+ Key: fileKey
46300
+ }));
46301
+ Affirm_default(response.Body, "Failed to fetch object from S3");
46302
+ return await StreamReader_default.read(response.Body, request, index);
46303
+ };
45717
46304
  this._listFiles = async (fileKeyPattern, maxKeys, continuationToken) => {
45718
46305
  Affirm_default(this._client, 'S3 client not yet initialized, call "connect()" first');
45719
46306
  let prefix2 = "";
@@ -45843,22 +46430,22 @@ var init_S3SourceDriver = __esm({
45843
46430
  });
45844
46431
 
45845
46432
  // ../../packages/drivers/src/s3/S3DestinationDriver.ts
45846
- var import_client_s33, import_path12, import_fs14, import_readline5, S3DestinationDriver;
46433
+ var import_client_s33, import_path12, import_fs14, S3DestinationDriver;
45847
46434
  var init_S3DestinationDriver = __esm({
45848
46435
  "../../packages/drivers/src/s3/S3DestinationDriver.ts"() {
45849
46436
  import_client_s33 = require("@aws-sdk/client-s3");
45850
46437
  init_src();
45851
46438
  init_src2();
46439
+ init_src4();
45852
46440
  init_AwsCredentials();
45853
46441
  import_path12 = __toESM(require("path"), 1);
45854
46442
  import_fs14 = __toESM(require("fs"), 1);
45855
- import_readline5 = __toESM(require("readline"), 1);
45856
46443
  S3DestinationDriver = class {
45857
46444
  constructor() {
45858
46445
  this.init = async (source) => {
45859
- this._bucketName = source.authentication["bucket"];
46446
+ this._bucketName = SecretManager_default.replaceSecret(source.authentication["bucket"]);
45860
46447
  const config = {
45861
- region: source.authentication["region"],
46448
+ region: SecretManager_default.replaceSecret(source.authentication["region"]),
45862
46449
  credentials: resolveAwsCredentials(source.authentication)
45863
46450
  };
45864
46451
  this._client = new import_client_s33.S3Client(config);
@@ -45898,9 +46485,8 @@ var init_S3DestinationDriver = __esm({
45898
46485
  Affirm_default(toName, "Invalid destination name");
45899
46486
  Affirm_default(import_fs14.default.existsSync(fromPath), `Source file does not exist: ${fromPath}`);
45900
46487
  const reader = import_fs14.default.createReadStream(fromPath);
45901
- const lineReader = import_readline5.default.createInterface({ input: reader, crlfDelay: Infinity });
45902
46488
  const transformedLines = async function* () {
45903
- for await (const line of lineReader) {
46489
+ for await (const line of readLines(reader)) {
45904
46490
  yield transform(line) + "\n";
45905
46491
  }
45906
46492
  };
@@ -45983,6 +46569,382 @@ var init_S3DestinationDriver = __esm({
45983
46569
  }
45984
46570
  });
45985
46571
 
46572
+ // ../../packages/drivers/src/presigned/UrlMinter.ts
46573
+ var DEFAULT_TIMEOUT_MS, DEFAULT_URL_FIELD, DEFAULT_FILE_NAME_PARAM, MAX_ERROR_BODY_CHARS, UrlMinterClass, UrlMinter, UrlMinter_default;
46574
+ var init_UrlMinter = __esm({
46575
+ "../../packages/drivers/src/presigned/UrlMinter.ts"() {
46576
+ init_src();
46577
+ init_src4();
46578
+ init_src2();
46579
+ init_OidcTokenProvider();
46580
+ DEFAULT_TIMEOUT_MS = 3e4;
46581
+ DEFAULT_URL_FIELD = "url";
46582
+ DEFAULT_FILE_NAME_PARAM = "filename";
46583
+ MAX_ERROR_BODY_CHARS = 500;
46584
+ UrlMinterClass = class {
46585
+ constructor() {
46586
+ /**
46587
+ * Mints a signed URL for one file name.
46588
+ * Throws with the endpoint's own answer when it refuses — a mint that fails is a transfer that
46589
+ * must not be attempted.
46590
+ */
46591
+ this.mint = async (auth, fileName) => {
46592
+ Affirm_default(auth, "Missing authentication config for the presigned-url source");
46593
+ Affirm_default(fileName, "Missing the file name to mint a signed URL for");
46594
+ const endpoint = SecretManager_default.replaceSecret(auth.url);
46595
+ Affirm_default(endpoint, "A presigned-url source requires its minting endpoint in authentication.url");
46596
+ const payload = await this._request(auth, endpoint, fileName);
46597
+ const urlField = auth.signedUrlField || DEFAULT_URL_FIELD;
46598
+ const url = this._resolve(payload, urlField);
46599
+ Affirm_default(url, `The minting endpoint ${this.safeUrl(endpoint)} returned no signed URL at "${urlField}". Set authentication.signedUrlField to where the URL is in its response.`);
46600
+ Affirm_default(typeof url === "string", `The minting endpoint returned a ${typeof url} at "${urlField}", not a URL string`);
46601
+ const headers = auth.signedHeadersField ? this._resolve(payload, auth.signedHeadersField) : void 0;
46602
+ Affirm_default(!headers || typeof headers === "object", `The minting endpoint returned a ${typeof headers} at "${auth.signedHeadersField}", not an object of headers`);
46603
+ Logger_default.log(`Minted a signed URL for "${fileName}" from ${this.safeUrl(endpoint)}: ${this.safeUrl(url)}`);
46604
+ return { url, headers: headers ?? {} };
46605
+ };
46606
+ /**
46607
+ * A URL with its query string removed, for logging. A signed URL carries its credentials there,
46608
+ * so logging one whole would put a working upload token in the log pipeline — the same rule
46609
+ * `StateEmitter` applies to every URL it emits.
46610
+ */
46611
+ this.safeUrl = (url) => {
46612
+ try {
46613
+ const parsed = new URL(url);
46614
+ return `${parsed.origin}${parsed.pathname}`;
46615
+ } catch {
46616
+ return "(unparseable URL)";
46617
+ }
46618
+ };
46619
+ this._request = async (auth, endpoint, fileName) => {
46620
+ const method = auth.httpMethod || "POST";
46621
+ const headers = await this._authHeaders(auth);
46622
+ const fileNameParam = auth.fileNameParam || DEFAULT_FILE_NAME_PARAM;
46623
+ const url = new URL(endpoint);
46624
+ for (const [key, value] of Object.entries(auth.queryParams ?? {}))
46625
+ url.searchParams.append(key, SecretManager_default.replaceSecret(value));
46626
+ const options2 = {
46627
+ method,
46628
+ headers,
46629
+ signal: AbortSignal.timeout(auth.timeout || DEFAULT_TIMEOUT_MS)
46630
+ };
46631
+ if (method === "POST" || method === "PUT" || method === "PATCH") {
46632
+ headers["Content-Type"] = headers["Content-Type"] || "application/json";
46633
+ options2.body = JSON.stringify({ [fileNameParam]: fileName });
46634
+ } else {
46635
+ url.searchParams.append(fileNameParam, fileName);
46636
+ }
46637
+ const response = await fetch(url.toString(), options2);
46638
+ if (!response.ok) {
46639
+ const body = await response.text().catch(() => "");
46640
+ const detail = body ? `: ${body.slice(0, MAX_ERROR_BODY_CHARS)}` : "";
46641
+ throw new Error(`The minting endpoint ${this.safeUrl(endpoint)} answered HTTP ${response.status} ${response.statusText} for "${fileName}"${detail}`);
46642
+ }
46643
+ return await response.json();
46644
+ };
46645
+ /**
46646
+ * The credentials for the MINT call. Every method here already exists elsewhere in the codebase —
46647
+ * `oidc` is the same client-credentials provider delta-share uses, which is what an API Gateway
46648
+ * fronted by M2M authentication wants, and its token is cached and refreshed across runs.
46649
+ */
46650
+ this._authHeaders = async (auth) => {
46651
+ const headers = {};
46652
+ for (const [key, value] of Object.entries(auth.headers ?? {}))
46653
+ headers[key] = SecretManager_default.replaceSecret(value);
46654
+ switch (auth.method) {
46655
+ case "oidc": {
46656
+ headers["Authorization"] = `Bearer ${await OidcTokenProvider_default.getToken(this._oidcConfig(auth))}`;
46657
+ break;
46658
+ }
46659
+ case "bearer-token": {
46660
+ const token2 = SecretManager_default.replaceSecret(auth.bearerToken);
46661
+ Affirm_default(token2, 'Authentication method "bearer-token" requires authentication.bearerToken');
46662
+ headers["Authorization"] = `Bearer ${token2}`;
46663
+ break;
46664
+ }
46665
+ case "api-key": {
46666
+ const apiKey = SecretManager_default.replaceSecret(auth.apiKey);
46667
+ Affirm_default(apiKey, 'Authentication method "api-key" requires authentication.apiKey');
46668
+ headers[auth.apiKeyHeader || "X-API-Key"] = apiKey;
46669
+ break;
46670
+ }
46671
+ case "username-password": {
46672
+ const user = SecretManager_default.replaceSecret(auth.user);
46673
+ const password = SecretManager_default.replaceSecret(auth.password);
46674
+ Affirm_default(user, 'Authentication method "username-password" requires authentication.user');
46675
+ Affirm_default(password, 'Authentication method "username-password" requires authentication.password');
46676
+ headers["Authorization"] = `Basic ${Buffer.from(`${user}:${password}`).toString("base64")}`;
46677
+ break;
46678
+ }
46679
+ case "none":
46680
+ break;
46681
+ default:
46682
+ throw new Error(`Authentication method "${auth.method}" is not supported for a presigned-url source. Use oidc, bearer-token, api-key, username-password or none.`);
46683
+ }
46684
+ return headers;
46685
+ };
46686
+ this._oidcConfig = (auth) => {
46687
+ const config = {
46688
+ tokenEndpoint: SecretManager_default.replaceSecret(auth.tokenEndpoint),
46689
+ clientId: SecretManager_default.replaceSecret(auth.clientId),
46690
+ clientSecret: SecretManager_default.replaceSecret(auth.clientSecret),
46691
+ scope: SecretManager_default.replaceSecret(auth.scope),
46692
+ audience: SecretManager_default.replaceSecret(auth.audience),
46693
+ clientAuthMethod: auth.clientAuthMethod
46694
+ };
46695
+ Affirm_default(config.tokenEndpoint, 'Authentication method "oidc" requires authentication.tokenEndpoint');
46696
+ Affirm_default(config.clientId, 'Authentication method "oidc" requires authentication.clientId');
46697
+ Affirm_default(config.clientSecret, 'Authentication method "oidc" requires authentication.clientSecret');
46698
+ return config;
46699
+ };
46700
+ /** Walks a dot path (`data.uploadUrl`) into the response, answering undefined rather than throwing. */
46701
+ this._resolve = (payload, path20) => {
46702
+ let current = payload;
46703
+ for (const segment of path20.split(".")) {
46704
+ if (current === null || typeof current !== "object")
46705
+ return void 0;
46706
+ current = current[segment];
46707
+ }
46708
+ return current;
46709
+ };
46710
+ }
46711
+ };
46712
+ UrlMinter = new UrlMinterClass();
46713
+ UrlMinter_default = UrlMinter;
46714
+ }
46715
+ });
46716
+
46717
+ // ../../packages/drivers/src/presigned/PresignedUrlSourceDriver.ts
46718
+ var import_stream4, import_promises12, import_fs15, MAX_ERROR_BODY_CHARS2, PresignedUrlSourceDriver;
46719
+ var init_PresignedUrlSourceDriver = __esm({
46720
+ "../../packages/drivers/src/presigned/PresignedUrlSourceDriver.ts"() {
46721
+ init_src();
46722
+ init_src4();
46723
+ init_src2();
46724
+ import_stream4 = require("stream");
46725
+ import_promises12 = require("stream/promises");
46726
+ import_fs15 = __toESM(require("fs"), 1);
46727
+ init_StreamReader();
46728
+ init_UrlMinter();
46729
+ MAX_ERROR_BODY_CHARS2 = 500;
46730
+ PresignedUrlSourceDriver = class {
46731
+ constructor() {
46732
+ this.init = async (source) => {
46733
+ Affirm_default(source, "Invalid source");
46734
+ Affirm_default(source.authentication, `Missing authentication for source "${source.name}"`);
46735
+ Affirm_default(source.authentication.url, `Source "${source.name}" is a presigned-url source and needs its minting endpoint in authentication.url`);
46736
+ this._source = source;
46737
+ this._auth = source.authentication;
46738
+ Logger_default.log(`Presigned URL source "${source.name}" initialised: minting endpoint=${UrlMinter_default.safeUrl(SecretManager_default.replaceSecret(source.authentication.url))}`);
46739
+ return this;
46740
+ };
46741
+ this.execute = async (_sql) => {
46742
+ void _sql;
46743
+ throw new Error("PresignedUrlSourceDriver.execute is not supported: a signed URL addresses a file, not a SQL engine");
46744
+ };
46745
+ this.query = async (_sql, _values) => {
46746
+ void _sql;
46747
+ void _values;
46748
+ throw new Error("PresignedUrlSourceDriver.query is not supported: a signed URL addresses a file, not a SQL engine");
46749
+ };
46750
+ this.exist = async (producer) => {
46751
+ Affirm_default(producer, "Invalid producer");
46752
+ const fileKey = producer.settings?.fileKey;
46753
+ Affirm_default(fileKey, `Producer "${producer.name}" has no settings.fileKey to mint a signed URL for`);
46754
+ try {
46755
+ const minted = await UrlMinter_default.mint(this._auth, fileKey);
46756
+ const response = await fetch(minted.url, {
46757
+ method: "GET",
46758
+ headers: { ...minted.headers, range: "bytes=0-0" }
46759
+ });
46760
+ await response.body?.cancel();
46761
+ return response.ok;
46762
+ } catch (error) {
46763
+ Logger_default.log(`Existence check for "${fileKey}" on source "${this._source.name}" answered no: ${error}`);
46764
+ return false;
46765
+ }
46766
+ };
46767
+ this.readAll = async (request) => {
46768
+ Affirm_default(request, "Invalid download request");
46769
+ Affirm_default(request.fileKey, "Invalid file key for download request");
46770
+ Affirm_default(request.fileType, "Invalid file type for download request");
46771
+ return await this._read(request);
46772
+ };
46773
+ this.readLinesInRange = async (request) => {
46774
+ Affirm_default(request, "Invalid read request");
46775
+ Affirm_default(request.fileKey, "Invalid file key for read request");
46776
+ Affirm_default(request.fileType, "Invalid file type for read request");
46777
+ Affirm_default(request.options, "Invalid read request options");
46778
+ Affirm_default.hasValue(request.options.lineFrom, "Invalid read request option lineFrom");
46779
+ Affirm_default.hasValue(request.options.lineTo, "Invalid read request option lineTo");
46780
+ return await this._read(request);
46781
+ };
46782
+ /**
46783
+ * Stages the producer's file locally, which is all a run needs from this source: everything after
46784
+ * it reads the staged path and never learns where the bytes came from.
46785
+ */
46786
+ this.ready = async (request) => {
46787
+ Affirm_default(request, "Invalid ready request");
46788
+ Affirm_default(request.producer, "Invalid producer");
46789
+ Affirm_default(request.scope, "Invalid executor scope");
46790
+ const { producer, scope, onProgress } = request;
46791
+ const fileKey = producer.settings?.fileKey;
46792
+ Affirm_default(fileKey, `Producer "${producer.name}" has no settings.fileKey to mint a signed URL for`);
46793
+ this._affirmSingleFile(fileKey);
46794
+ const localPath = ExecutorScope_default.getProducerPath(scope, producer, fileKey);
46795
+ ExecutorScope_default.ensurePath(localPath);
46796
+ const minted = await UrlMinter_default.mint(this._auth, fileKey);
46797
+ const response = await this._fetchSigned(minted, fileKey);
46798
+ const totalBytes = Number(response.headers.get("content-length") ?? 0);
46799
+ let stagedBytes = 0;
46800
+ const meter = new import_stream4.Transform({
46801
+ transform(chunk, _encoding, done) {
46802
+ stagedBytes += chunk.length;
46803
+ if (totalBytes > 0)
46804
+ onProgress?.(Math.min(1, stagedBytes / totalBytes));
46805
+ done(null, chunk);
46806
+ }
46807
+ });
46808
+ await (0, import_promises12.pipeline)(import_stream4.Readable.fromWeb(response.body), meter, import_fs15.default.createWriteStream(localPath));
46809
+ onProgress?.(1);
46810
+ const fileSize = import_fs15.default.statSync(localPath).size;
46811
+ Logger_default.info(`Staged "${fileKey}" from a signed URL to ${localPath} (${fileSize} bytes)`);
46812
+ return { files: [{ fullUri: localPath, fileSize }] };
46813
+ };
46814
+ this._read = async (request) => {
46815
+ this._affirmSingleFile(request.fileKey);
46816
+ const minted = await UrlMinter_default.mint(this._auth, request.fileKey);
46817
+ const response = await this._fetchSigned(minted, request.fileKey);
46818
+ return await StreamReader_default.read(import_stream4.Readable.fromWeb(response.body), request);
46819
+ };
46820
+ /**
46821
+ * A minting endpoint signs one named object, so there is nothing for a `%` pattern to expand
46822
+ * against — no listing exists on this side. Refused here rather than silently staging the one
46823
+ * file whose name happens to contain the pattern.
46824
+ */
46825
+ this._affirmSingleFile = (fileKey) => {
46826
+ Affirm_default(
46827
+ !fileKey.includes("%"),
46828
+ `A presigned-url source cannot expand the file key pattern "${fileKey}": the minting endpoint signs one named file and this driver cannot list the others. Name the file exactly, or use a source that can list (aws-s3, local).`
46829
+ );
46830
+ };
46831
+ this._fetchSigned = async (minted, fileKey) => {
46832
+ const response = await fetch(minted.url, {
46833
+ method: this._auth.signedUrlMethod || "GET",
46834
+ headers: minted.headers
46835
+ });
46836
+ if (!response.ok) {
46837
+ const body = await response.text().catch(() => "");
46838
+ const detail = body ? `: ${body.slice(0, MAX_ERROR_BODY_CHARS2)}` : "";
46839
+ throw new Error(`The signed URL for "${fileKey}" answered HTTP ${response.status} ${response.statusText}${detail}`);
46840
+ }
46841
+ Affirm_default(response.body, `The signed URL for "${fileKey}" returned no body`);
46842
+ return response;
46843
+ };
46844
+ }
46845
+ };
46846
+ }
46847
+ });
46848
+
46849
+ // ../../packages/drivers/src/presigned/PresignedUrlDestinationDriver.ts
46850
+ var import_stream5, import_crypto5, import_promises13, import_fs16, MAX_ERROR_BODY_CHARS3, PresignedUrlDestinationDriver;
46851
+ var init_PresignedUrlDestinationDriver = __esm({
46852
+ "../../packages/drivers/src/presigned/PresignedUrlDestinationDriver.ts"() {
46853
+ init_src();
46854
+ init_src4();
46855
+ init_src2();
46856
+ import_stream5 = require("stream");
46857
+ import_crypto5 = require("crypto");
46858
+ import_promises13 = __toESM(require("fs/promises"), 1);
46859
+ import_fs16 = __toESM(require("fs"), 1);
46860
+ init_UrlMinter();
46861
+ MAX_ERROR_BODY_CHARS3 = 500;
46862
+ PresignedUrlDestinationDriver = class {
46863
+ constructor() {
46864
+ this.init = async (source) => {
46865
+ Affirm_default(source, "Invalid source");
46866
+ Affirm_default(source.authentication, `Missing authentication for source "${source.name}"`);
46867
+ Affirm_default(source.authentication.url, `Source "${source.name}" is a presigned-url destination and needs its minting endpoint in authentication.url`);
46868
+ this._source = source;
46869
+ this._auth = source.authentication;
46870
+ Logger_default.log(`Presigned URL destination "${source.name}" initialised: minting endpoint=${UrlMinter_default.safeUrl(SecretManager_default.replaceSecret(source.authentication.url))}`);
46871
+ return this;
46872
+ };
46873
+ this.move = async (fromPath, toName) => {
46874
+ Affirm_default(fromPath, "Invalid source path");
46875
+ Affirm_default(toName, "Invalid destination name");
46876
+ Affirm_default(import_fs16.default.existsSync(fromPath), `Source file does not exist: ${fromPath}`);
46877
+ try {
46878
+ return await this._upload(fromPath, toName);
46879
+ } catch (error) {
46880
+ Logger_default.error(`Presigned upload of "${toName}" from source "${this._source.name}" failed: ${error}`);
46881
+ return { res: false, key: "", bucket: "" };
46882
+ }
46883
+ };
46884
+ this.transformAndMove = async (fromPath, transform, toName) => {
46885
+ Affirm_default(fromPath, "Invalid source path");
46886
+ Affirm_default(transform, "Invalid transform function");
46887
+ Affirm_default(toName, "Invalid destination name");
46888
+ Affirm_default(import_fs16.default.existsSync(fromPath), `Source file does not exist: ${fromPath}`);
46889
+ const stagingPath = `${fromPath}.${(0, import_crypto5.randomUUID)()}.partial`;
46890
+ try {
46891
+ await this._stageTransformed(fromPath, transform, stagingPath);
46892
+ return await this._upload(stagingPath, toName);
46893
+ } catch (error) {
46894
+ Logger_default.error(`Presigned upload of "${toName}" from source "${this._source.name}" failed: ${error}`);
46895
+ return { res: false, key: "", bucket: "" };
46896
+ } finally {
46897
+ await this._discardStaging(stagingPath);
46898
+ }
46899
+ };
46900
+ this._upload = async (fromPath, toName) => {
46901
+ const minted = await UrlMinter_default.mint(this._auth, toName);
46902
+ const fileSize = import_fs16.default.statSync(fromPath).size;
46903
+ const options2 = {
46904
+ method: this._auth.signedUrlMethod || "PUT",
46905
+ headers: { ...minted.headers, "content-length": String(fileSize) },
46906
+ body: import_stream5.Readable.toWeb(import_fs16.default.createReadStream(fromPath)),
46907
+ duplex: "half"
46908
+ };
46909
+ const response = await fetch(minted.url, options2);
46910
+ if (!response.ok) {
46911
+ const body = await response.text().catch(() => "");
46912
+ const detail = body ? `: ${body.slice(0, MAX_ERROR_BODY_CHARS3)}` : "";
46913
+ throw new Error(`The signed URL for "${toName}" answered HTTP ${response.status} ${response.statusText}${detail}`);
46914
+ }
46915
+ Logger_default.info(`Uploaded "${toName}" (${fileSize} bytes) to ${UrlMinter_default.safeUrl(minted.url)}`);
46916
+ return { res: true, key: toName, bucket: this._host(minted.url) };
46917
+ };
46918
+ /** Writes the transformed lines to their own file, so the upload has a size to declare. */
46919
+ this._stageTransformed = async (fromPath, transform, stagingPath) => {
46920
+ const reader = import_fs16.default.createReadStream(fromPath);
46921
+ const writer = new BatchWriter_default(import_fs16.default.createWriteStream(stagingPath));
46922
+ for await (const line of readLines(reader)) {
46923
+ if (!writer.write(transform(line)))
46924
+ await writer.waitForDrain();
46925
+ }
46926
+ await writer.close();
46927
+ reader.close();
46928
+ };
46929
+ /** Removes a staging file the export left behind; never masks the error that is being reported. */
46930
+ this._discardStaging = async (stagingPath) => {
46931
+ try {
46932
+ await import_promises13.default.unlink(stagingPath);
46933
+ } catch {
46934
+ }
46935
+ };
46936
+ this._host = (url) => {
46937
+ try {
46938
+ return new URL(url).host;
46939
+ } catch {
46940
+ return "";
46941
+ }
46942
+ };
46943
+ }
46944
+ };
46945
+ }
46946
+ });
46947
+
45986
46948
  // ../../packages/drivers/src/DriverFactory.ts
45987
46949
  var STAGEABLE_ENGINES, SQL_ENGINES, DriverFactoryClass, DriverFactory, DriverFactory_default;
45988
46950
  var init_DriverFactory = __esm({
@@ -45995,7 +46957,9 @@ var init_DriverFactory = __esm({
45995
46957
  init_LocalDestinationDriver();
45996
46958
  init_S3SourceDriver();
45997
46959
  init_S3DestinationDriver();
45998
- STAGEABLE_ENGINES = ["local", "aws-s3", "delta-share", "aws-redshift", "postgres"];
46960
+ init_PresignedUrlSourceDriver();
46961
+ init_PresignedUrlDestinationDriver();
46962
+ STAGEABLE_ENGINES = ["local", "aws-s3", "delta-share", "aws-redshift", "postgres", "presigned-url"];
45999
46963
  SQL_ENGINES = ["aws-redshift", "postgres"];
46000
46964
  DriverFactoryClass = class {
46001
46965
  constructor() {
@@ -46035,6 +46999,11 @@ var init_DriverFactory = __esm({
46035
46999
  await driver.init(source);
46036
47000
  return driver;
46037
47001
  }
47002
+ case "presigned-url": {
47003
+ const driver = new PresignedUrlSourceDriver();
47004
+ await driver.init(source);
47005
+ return driver;
47006
+ }
46038
47007
  default:
46039
47008
  throw new Error(`Invalid driver type "${source.engine}". This driver is not implemented yet`);
46040
47009
  }
@@ -46046,6 +47015,11 @@ var init_DriverFactory = __esm({
46046
47015
  await driver.init(source);
46047
47016
  return driver;
46048
47017
  }
47018
+ case "presigned-url": {
47019
+ const driver = new PresignedUrlDestinationDriver();
47020
+ await driver.init(source);
47021
+ return driver;
47022
+ }
46049
47023
  case "local": {
46050
47024
  const driver = new LocalDestinationDriver();
46051
47025
  await driver.init(source);
@@ -46076,9 +47050,16 @@ var init_src8 = __esm({
46076
47050
  init_LocalSourceDriver();
46077
47051
  init_S3DestinationDriver();
46078
47052
  init_S3SourceDriver();
47053
+ init_PresignedUrlDestinationDriver();
47054
+ init_PresignedUrlSourceDriver();
47055
+ init_StreamReader();
47056
+ init_UrlMinter();
46079
47057
  init_PostgresDriver();
46080
47058
  init_SqlDatasetWriter();
47059
+ init_SqlPredicate();
47060
+ init_SqlPushdown();
46081
47061
  init_SqlQueryBuilder();
47062
+ init_SqlSchemaReader();
46082
47063
  }
46083
47064
  });
46084
47065
 
@@ -46216,7 +47197,7 @@ var init_ProducerManager = __esm({
46216
47197
  });
46217
47198
 
46218
47199
  // ../../packages/engines/src/transform/TypeCaster.ts
46219
- var import_dayjs, import_customParseFormat, import_utc, DAY_OF_YEAR_TOKEN, FORMAT_ONLY_TOKENS, DATE_PARSE_TOKENS, TypeCasterClass, TypeCaster, TypeCaster_default;
47200
+ var import_dayjs, import_customParseFormat, import_utc, DAY_OF_YEAR_TOKEN, FORMAT_ONLY_TOKENS, DATE_PARSE_TOKENS, TYPE_DEFAULTS, TypeCasterClass, TypeCaster, TypeCaster_default;
46220
47201
  var init_TypeCaster = __esm({
46221
47202
  "../../packages/engines/src/transform/TypeCaster.ts"() {
46222
47203
  init_src();
@@ -46237,7 +47218,36 @@ var init_TypeCaster = __esm({
46237
47218
  { token: "mm", width: 2 },
46238
47219
  { token: "ss", width: 2 }
46239
47220
  ];
47221
+ TYPE_DEFAULTS = {
47222
+ string: "",
47223
+ number: 0,
47224
+ boolean: false,
47225
+ datetime: null
47226
+ };
46240
47227
  TypeCasterClass = class {
47228
+ constructor() {
47229
+ /**
47230
+ * Casts a value for a DECLARED dimension: the type cast, plus what a MISSING value means for it.
47231
+ *
47232
+ * Missing is null/undefined, and — for every type but `string` — the empty string, which is how
47233
+ * a NULL arrives from every source that stages through CSV (SQL, Delta Share, CSV files) and is
47234
+ * indistinguishable from one there. An empty string IS a value for a `string` dimension, so it
47235
+ * is left alone. A missing value reads as `null`, unless the dimension declares `nullable: false`
47236
+ * and so asks for its type's own empty value instead. Without this, a NULL in a number column
47237
+ * went through `Number('')` and arrived as a real `0` that joins, sums and filters like one the
47238
+ * source actually held.
47239
+ */
47240
+ this.castDimension = (value, dimension) => {
47241
+ if (dimension.type && this.isMissing(value, dimension.type))
47242
+ return dimension.nullable === false ? TYPE_DEFAULTS[dimension.type] : null;
47243
+ return this.cast(value, dimension.type, dimension.format);
47244
+ };
47245
+ /** null/undefined for any type, plus the empty string for every type but `string`. */
47246
+ this.isMissing = (value, type) => {
47247
+ if (!Algo_default.hasVal(value)) return true;
47248
+ return value === "" && type !== "string";
47249
+ };
47250
+ }
46241
47251
  /**
46242
47252
  * Casts the value to the requested type (only if needed)
46243
47253
  * Optional format parameter currently supports:
@@ -46427,7 +47437,7 @@ var init_LineParser = __esm({
46427
47437
  counter = performance.now();
46428
47438
  const value = {};
46429
47439
  for (const dim of dimensions) {
46430
- value[dim.name] = TypeCaster_default.cast(parts[dim.index], dim.prodDimension.type, dim.prodDimension.format);
47440
+ value[dim.name] = TypeCaster_default.castDimension(parts[dim.index], dim.prodDimension);
46431
47441
  }
46432
47442
  tracker.measure("process-line:cast&build-record", performance.now() - counter);
46433
47443
  return value;
@@ -46448,7 +47458,7 @@ var init_LineParser = __esm({
46448
47458
  const value = {};
46449
47459
  for (const dim of dimensions) {
46450
47460
  const key = dim.prodDimension.alias ?? dim.prodDimension.name;
46451
- value[dim.name] = TypeCaster_default.cast(parsed[key], dim.prodDimension.type, dim.prodDimension.format);
47461
+ value[dim.name] = TypeCaster_default.castDimension(parsed[key], dim.prodDimension);
46452
47462
  }
46453
47463
  tracker.measure("process-line:cast&build-record", performance.now() - counter);
46454
47464
  return value;
@@ -46470,8 +47480,11 @@ var init_LineParser = __esm({
46470
47480
  }
46471
47481
  if (!dim.range && !dim.placement) continue;
46472
47482
  const range2 = dim.placement ? FixedWidthParser_default.resolvePlacement(dim.placement, layout, occurrence) : dim.range;
46473
- const raw = range2 ? FixedWidthParser_default.readField(line, range2) : null;
46474
- record[dim.name] = TypeCaster_default.cast(raw, dim.prodDimension.type, dim.prodDimension.format);
47483
+ if (!range2) {
47484
+ record[dim.name] = null;
47485
+ continue;
47486
+ }
47487
+ record[dim.name] = TypeCaster_default.castDimension(FixedWidthParser_default.readField(line, range2), dim.prodDimension);
46475
47488
  }
46476
47489
  return record;
46477
47490
  };
@@ -46483,7 +47496,7 @@ var init_LineParser = __esm({
46483
47496
  const record = {};
46484
47497
  for (const [index, field] of fields.entries()) {
46485
47498
  const fieldKey = field.finalKey;
46486
- record[fieldKey] = TypeCaster_default.cast(parts[index], field.dimension?.type ?? "string");
47499
+ record[fieldKey] = field.dimension ? TypeCaster_default.castDimension(parts[index], field.dimension) : TypeCaster_default.cast(parts[index], "string");
46487
47500
  }
46488
47501
  return record;
46489
47502
  };
@@ -46501,11 +47514,11 @@ var init_LineParser = __esm({
46501
47514
  });
46502
47515
 
46503
47516
  // ../../packages/engines/src/producer/ProducerEngine.ts
46504
- var import_path13, import_crypto4, noopTracker, ProducerEngineClass, ProducerEngine, ProducerEngine_default;
47517
+ var import_path13, import_crypto6, noopTracker, ProducerEngineClass, ProducerEngine, ProducerEngine_default;
46505
47518
  var init_ProducerEngine = __esm({
46506
47519
  "../../packages/engines/src/producer/ProducerEngine.ts"() {
46507
47520
  import_path13 = __toESM(require("path"), 1);
46508
- import_crypto4 = require("crypto");
47521
+ import_crypto6 = require("crypto");
46509
47522
  init_src();
46510
47523
  init_src8();
46511
47524
  init_src4();
@@ -46591,7 +47604,7 @@ var init_ProducerEngine = __esm({
46591
47604
  switch (source.engine) {
46592
47605
  case "aws-redshift":
46593
47606
  case "postgres": {
46594
- const sql = `${SqlQueryBuilder_default.buildProducerSelect(producer, source, sampleSize)}`;
47607
+ const sql = SqlQueryBuilder_default.buildProducerSelect(producer, source, { limit: sampleSize });
46595
47608
  Affirm_default(sql, `Invalid SQL from deployment compilation for producer "${producer.name}"`);
46596
47609
  const driver = await DriverFactory_default.instantiateSource(source);
46597
47610
  Affirm_default(driver, `No driver found for producer "${producer.name}" with driver type "${source.engine}"`);
@@ -46608,7 +47621,7 @@ var init_ProducerEngine = __esm({
46608
47621
  let effectiveProducer = producer;
46609
47622
  let effectiveSource = source;
46610
47623
  if (compressionType) {
46611
- scope = { id: (0, import_crypto4.randomUUID)(), folder: `sample-${(0, import_crypto4.randomUUID)()}`, workersId: [] };
47624
+ scope = { id: (0, import_crypto6.randomUUID)(), folder: `sample-${(0, import_crypto6.randomUUID)()}`, workersId: [] };
46612
47625
  const driver = await DriverFactory_default.instantiateSource(source);
46613
47626
  const readyResult = await driver.ready({ producer, scope });
46614
47627
  const firstFile = readyResult.files[0].fullUri;
@@ -46660,7 +47673,7 @@ var init_ProducerEngine = __esm({
46660
47673
  const record = {};
46661
47674
  for (const dim of producer.dimensions) {
46662
47675
  const key = dim.alias ?? dim.name;
46663
- record[dim.name] = TypeCaster_default.cast(item[key], dim.type, dim.format);
47676
+ record[dim.name] = TypeCaster_default.castDimension(item[key], dim);
46664
47677
  }
46665
47678
  return ProducerManager_default.applyFills(record, fills, sourceFilename);
46666
47679
  });
@@ -46767,53 +47780,178 @@ var init_ProducerEngine = __esm({
46767
47780
  }
46768
47781
  });
46769
47782
 
47783
+ // ../../packages/engines/src/producer/DiscoverMerger.ts
47784
+ var DiscoverMergerClass, DiscoverMerger, DiscoverMerger_default;
47785
+ var init_DiscoverMerger = __esm({
47786
+ "../../packages/engines/src/producer/DiscoverMerger.ts"() {
47787
+ init_src();
47788
+ DiscoverMergerClass = class {
47789
+ constructor() {
47790
+ this.merge = (authored, discovered, mode = "merge") => {
47791
+ Affirm_default(Array.isArray(authored), "Invalid authored dimensions");
47792
+ Affirm_default.hasItems(discovered, "Discover produced no dimensions to merge");
47793
+ Affirm_default(mode === "merge" || mode === "replace", `Unknown discover mode "${mode}" \u2014 expected "merge" or "replace".`);
47794
+ const pairs = authored.map((dimension) => ({ dimension, found: this._lookup(discovered, dimension) }));
47795
+ const consumed = new Set(pairs.map((pair) => pair.found?.name).filter(Boolean));
47796
+ const added = discovered.filter((field) => !consumed.has(field.name));
47797
+ const report = { added: added.map((field) => field.name), filled: [], orphaned: [], mismatched: [] };
47798
+ for (const { dimension, found } of pairs)
47799
+ this._reportMismatch(dimension, found, report);
47800
+ if (mode === "replace") {
47801
+ const unreadable = pairs.filter((pair) => !this._readsSource(pair.dimension)).map((pair) => pair.dimension);
47802
+ report.orphaned = pairs.filter((pair) => !pair.found && this._readsSource(pair.dimension)).map((pair) => pair.dimension.name);
47803
+ return { dimensions: [...discovered, ...unreadable], report };
47804
+ }
47805
+ const dimensions = pairs.map(({ dimension, found }) => {
47806
+ if (found) return this._fill(dimension, found, report);
47807
+ if (this._readsSource(dimension)) report.orphaned.push(dimension.name);
47808
+ return dimension;
47809
+ });
47810
+ return { dimensions: [...dimensions, ...added], report };
47811
+ };
47812
+ /**
47813
+ * What an authored dimension gains from the field discover matched it to: the properties it
47814
+ * declared NOTHING for, and nothing else. In particular `type` is never changed — a producer is
47815
+ * free to read a `timestamptz` as a string or a padded `char(n)` as one, and a re-discover that
47816
+ * undid that would be the same un-curation this merge exists to stop. The disagreement is
47817
+ * reported instead, so it is the author who decides.
47818
+ */
47819
+ this._fill = (authored, discovered, report) => {
47820
+ const filled = { ...authored };
47821
+ const properties = [];
47822
+ if (!filled.type && discovered.type) {
47823
+ filled.type = discovered.type;
47824
+ properties.push("type");
47825
+ }
47826
+ if (filled.nullable === void 0 && discovered.nullable === false) {
47827
+ filled.nullable = false;
47828
+ properties.push("nullable");
47829
+ }
47830
+ if (properties.length > 0) report.filled.push({ name: authored.name, properties });
47831
+ return filled;
47832
+ };
47833
+ this._reportMismatch = (authored, discovered, report) => {
47834
+ if (!discovered?.type) return;
47835
+ if (!authored.type || authored.type === discovered.type) return;
47836
+ report.mismatched.push({ name: authored.name, declared: authored.type, discovered: discovered.type });
47837
+ };
47838
+ /**
47839
+ * The discovered field an authored dimension reads, if any.
47840
+ *
47841
+ * A dimension names its source field with `alias`, falling back to its own `name`. Two
47842
+ * fallbacks past that: the engines fold an unquoted SQL identifier to lower case, so a
47843
+ * case-only difference is the same column; and an `alias` carrying a path (`address.city`)
47844
+ * reaches INTO a field discover reports by its root name, so the root is what it matches —
47845
+ * which also stops a decomposed object being re-added as a dimension of its own.
47846
+ */
47847
+ this._lookup = (discovered, dimension) => {
47848
+ const key = dimension.alias ?? dimension.name;
47849
+ if (!key) return void 0;
47850
+ const byName = (name) => discovered.find((field) => field.name === name) ?? discovered.find((field) => field.name.toLowerCase() === name.toLowerCase());
47851
+ return byName(key) ?? (key.includes(".") ? byName(key.split(".")[0]) : void 0);
47852
+ };
47853
+ /**
47854
+ * Whether this dimension is supposed to correspond to a field of the source at all. A group
47855
+ * produces no column of its own, and `sourceFilename` / `occurrenceOf` dimensions are filled by
47856
+ * the read rather than read from the data — none of them missing from a discovery is news.
47857
+ */
47858
+ this._readsSource = (dimension) => !dimension.dimensions && !dimension.sourceFilename && !dimension.occurrenceOf;
47859
+ }
47860
+ };
47861
+ DiscoverMerger = new DiscoverMergerClass();
47862
+ DiscoverMerger_default = DiscoverMerger;
47863
+ }
47864
+ });
47865
+
46770
47866
  // ../../packages/engines/src/ai/DeveloperEngine.ts
46771
- var import_path14, import_promises12, import_dayjs2, import_customParseFormat2, DeveloperEngineClass, DeveloperEngine, DeveloperEngine_default;
47867
+ var import_path14, import_promises14, import_dayjs2, import_customParseFormat2, DeveloperEngineClass, DeveloperEngine, DeveloperEngine_default;
46772
47868
  var init_DeveloperEngine = __esm({
46773
47869
  "../../packages/engines/src/ai/DeveloperEngine.ts"() {
46774
47870
  init_src();
46775
47871
  init_ProducerEngine();
47872
+ init_DiscoverMerger();
46776
47873
  init_src4();
46777
47874
  init_src8();
46778
47875
  import_path14 = __toESM(require("path"), 1);
46779
- import_promises12 = __toESM(require("fs/promises"), 1);
47876
+ import_promises14 = __toESM(require("fs/promises"), 1);
46780
47877
  import_dayjs2 = __toESM(require("dayjs"), 1);
46781
47878
  import_customParseFormat2 = __toESM(require("dayjs/plugin/customParseFormat"), 1);
46782
47879
  import_dayjs2.default.extend(import_customParseFormat2.default);
46783
47880
  DeveloperEngineClass = class {
46784
47881
  constructor() {
46785
- this.discover = async (producer) => {
47882
+ /**
47883
+ * Learns a producer's fields from its source and writes them back into the producer's config.
47884
+ *
47885
+ * The write is a MERGE by default (see `DiscoverMerger`): what was hand-authored on a dimension
47886
+ * survives a re-discover, and discover only supplies what the dimension declared nothing for.
47887
+ * `replace` rewrites the dimension list from what was found, which is what bootstrapping a
47888
+ * producer off the `create-producer` template needs.
47889
+ */
47890
+ this.discover = async (producer, mode = "merge") => {
46786
47891
  Affirm_default(producer, "Invalid producer");
46787
47892
  const source = Environment_default.getSource(producer.source);
46788
47893
  Affirm_default(source, `Cannot discover producer "${producer.name}": source "${producer.source}" was not found.`);
46789
47894
  await this.assertDiscoverable(producer, source);
46790
- const sampleData = await ProducerEngine_default.readSampleData(producer, 10, true);
46791
- Affirm_default(sampleData, "Discover process failed: no result found");
46792
- const typeDefinitions = this.extractFieldTypes(sampleData);
46793
- const mappedProducer = {
46794
- name: producer.name,
46795
- description: producer.description,
46796
- source: producer.source,
46797
- settings: { ...producer.settings },
46798
- dimensions: typeDefinitions.map((field) => ({
46799
- name: field.name,
46800
- type: this.mapFieldTypeToProducerType(field.type),
46801
- description: `Auto-mapped field: ${field.name}`,
46802
- classification: this.extractFieldClassification(field)?.isPHI ? ["PHI"] : void 0
46803
- })),
46804
- measures: [],
46805
- _version: producer._version ?? 1
46806
- };
46807
- mappedProducer["$schema"] = producer["$schema"];
47895
+ const typeDefinitions = DriverFactory_default.isSql(source.engine) ? await this.discoverSqlFields(producer, source) : this.extractFieldTypes(await ProducerEngine_default.readSampleData(producer, 10, true));
47896
+ Affirm_default.hasItems(typeDefinitions, `Discover found no fields for producer "${producer.name}" \u2014 there is nothing to write back.`);
46808
47897
  const producerPath = Environment_default.resolveResourceFilePath("producers", producer.name);
46809
- await import_promises12.default.mkdir(import_path14.default.dirname(producerPath), { recursive: true });
46810
- await import_promises12.default.writeFile(producerPath, JSON.stringify(mappedProducer, null, 4), "utf-8");
46811
- return { producer: mappedProducer, fields: typeDefinitions };
47898
+ const authored = await this.readAuthoredProducer(producerPath, producer);
47899
+ const discovered = typeDefinitions.map(this.toDimension);
47900
+ const { dimensions, report } = DiscoverMerger_default.merge(authored.dimensions ?? [], discovered, mode);
47901
+ const mappedProducer = { ...authored, dimensions };
47902
+ await import_promises14.default.mkdir(import_path14.default.dirname(producerPath), { recursive: true });
47903
+ await import_promises14.default.writeFile(producerPath, `${JSON.stringify(mappedProducer, null, 4)}
47904
+ `, "utf-8");
47905
+ return { producer: mappedProducer, fields: typeDefinitions, report };
47906
+ };
47907
+ /**
47908
+ * The producer exactly as it sits on disk. Discover rewrites one property of a file somebody
47909
+ * else wrote, so it starts from that file rather than from the loaded (expanded) resource.
47910
+ */
47911
+ this.readAuthoredProducer = async (producerPath, producer) => {
47912
+ const raw = await import_promises14.default.readFile(producerPath, "utf-8").catch(() => null);
47913
+ Affirm_default(raw, `Cannot discover producer "${producer.name}": its config file was not found at "${producerPath}".`);
47914
+ const authored = JSON.parse(raw);
47915
+ Affirm_default(authored?.name === producer.name, `Cannot discover producer "${producer.name}": "${producerPath}" declares producer "${authored?.name}".`);
47916
+ return authored;
47917
+ };
47918
+ /** The dimension discover would write for a field it found, before any merge decides what of it is used. */
47919
+ this.toDimension = (field) => ({
47920
+ name: field.name,
47921
+ type: field.dimensionType ?? this.mapFieldTypeToProducerType(field.type),
47922
+ // Only written when the source STATED the column is NOT NULL. `nullable` defaults to
47923
+ // true everywhere else, so leaving it off is the same declaration with less noise.
47924
+ nullable: field.nullable === false ? false : void 0,
47925
+ description: `Auto-mapped field: ${field.name}`,
47926
+ classification: this.extractFieldClassification(field)?.isPHI ? ["PHI"] : void 0
47927
+ });
47928
+ /**
47929
+ * The columns of a SQL producer's table, read from the source's own catalog.
47930
+ *
47931
+ * The catalog states the type, so nothing is inferred here: a `numeric` column is a number even
47932
+ * when every row sampled from it was NULL, and `is_nullable` is a fact no number of sample rows
47933
+ * could establish. Values are never read at all, which also means discovering a table costs one
47934
+ * small statement rather than a scan of it.
47935
+ */
47936
+ this.discoverSqlFields = async (producer, source) => {
47937
+ const driver = await DriverFactory_default.instantiateSource(source);
47938
+ Affirm_default(driver, `Cannot discover producer "${producer.name}": no driver available for engine "${source.engine}".`);
47939
+ const columns = await SqlSchemaReader_default.readColumns(driver, producer, source);
47940
+ return columns.map((column) => ({
47941
+ name: column.name,
47942
+ type: column.dataType,
47943
+ dimensionType: SqlSchemaReader_default.toDimensionType(column.dataType),
47944
+ nullable: column.nullable
47945
+ }));
46812
47946
  };
46813
47947
  // Discover-only precondition: sampling real data requires a configured, readable file.
46814
47948
  // Unlike the normal read path (which tolerates empty/absent files by design), discover
46815
47949
  // has nothing to infer from, so fail early with a message that says exactly what to fix.
46816
47950
  this.assertDiscoverable = async (producer, source) => {
47951
+ if (DriverFactory_default.isSql(source.engine)) {
47952
+ Affirm_default(producer.settings?.sqlTable, `Cannot discover producer "${producer.name}": no "sqlTable" is configured \u2014 set the table to read from source "${source.name}" first, as "table" or "schema.table".`);
47953
+ return;
47954
+ }
46817
47955
  const fileEngines = ["local", "aws-s3", "delta-share"];
46818
47956
  if (!fileEngines.includes(source.engine)) return;
46819
47957
  Affirm_default(producer.settings?.fileKey, `Cannot discover producer "${producer.name}": no "fileKey" is configured \u2014 set the file (or wildcard pattern) to read from source "${source.name}" first.`);
@@ -49344,10 +50482,10 @@ var init_MockDataFactory = __esm({
49344
50482
  });
49345
50483
 
49346
50484
  // ../../packages/engines/src/mock/SyntheticInputEngine.ts
49347
- var import_promises13, import_os, import_path15, import_dayjs4, import_customParseFormat3, import_utc3, POOL_CAP, SyntheticInputEngineClass, SyntheticInputEngine;
50485
+ var import_promises15, import_os, import_path15, import_dayjs4, import_customParseFormat3, import_utc3, POOL_CAP, SyntheticInputEngineClass, SyntheticInputEngine;
49348
50486
  var init_SyntheticInputEngine = __esm({
49349
50487
  "../../packages/engines/src/mock/SyntheticInputEngine.ts"() {
49350
- import_promises13 = __toESM(require("fs/promises"), 1);
50488
+ import_promises15 = __toESM(require("fs/promises"), 1);
49351
50489
  import_os = __toESM(require("os"), 1);
49352
50490
  import_path15 = __toESM(require("path"), 1);
49353
50491
  import_dayjs4 = __toESM(require("dayjs"), 1);
@@ -49373,7 +50511,7 @@ var init_SyntheticInputEngine = __esm({
49373
50511
  for (const cs2 of [...BUILTIN_CODE_SETS, ...codeSets]) codeSetMap.set(cs2.name.toLowerCase(), cs2);
49374
50512
  const producerMap = new Map(producers.map((p5) => [p5.name, p5]));
49375
50513
  const refTargets = this._collectReferences(producers, producerMap);
49376
- const dir = await import_promises13.default.mkdtemp(import_path15.default.join(import_os.default.tmpdir(), "remora-synth-"));
50514
+ const dir = await import_promises15.default.mkdtemp(import_path15.default.join(import_os.default.tmpdir(), "remora-synth-"));
49377
50515
  const poolSize = Math.min(Math.max(opts.records, 1), POOL_CAP);
49378
50516
  const registry = new MockKeyRegistry();
49379
50517
  const specs = producers.flatMap((p5) => this._specs(p5, codeSetMap, producerMap, refTargets));
@@ -49390,7 +50528,7 @@ var init_SyntheticInputEngine = __esm({
49390
50528
  return { inputs, dir };
49391
50529
  };
49392
50530
  this.cleanup = async (bundle) => {
49393
- if (bundle?.dir) await import_promises13.default.rm(bundle.dir, { recursive: true, force: true });
50531
+ if (bundle?.dir) await import_promises15.default.rm(bundle.dir, { recursive: true, force: true });
49394
50532
  };
49395
50533
  this._assertOpts = (opts) => {
49396
50534
  Affirm_default(opts, "Invalid options");
@@ -49440,8 +50578,8 @@ var init_SyntheticInputEngine = __esm({
49440
50578
  lines.push(JSON.stringify(row) + "\n");
49441
50579
  }
49442
50580
  const filePath = import_path15.default.join(dir, `${producer.name}.jsonl`);
49443
- await import_promises13.default.writeFile(filePath, lines.join(""), "utf-8");
49444
- const { size } = await import_promises13.default.stat(filePath);
50581
+ await import_promises15.default.writeFile(filePath, lines.join(""), "utf-8");
50582
+ const { size } = await import_promises15.default.stat(filePath);
49445
50583
  return { path: filePath, fileSize: size };
49446
50584
  };
49447
50585
  // Distinct structured code sets referenced by these specs -> their records (deduped by name).
@@ -49575,8 +50713,7 @@ var init_SyntheticInputEngine = __esm({
49575
50713
  return length;
49576
50714
  }
49577
50715
  Affirm_default(typeof value === "string", `Dimension "${dim.name}" synth "${prop}" must be a date string on a "datetime" dimension (got ${JSON.stringify(value)})`);
49578
- const formatted = dim.format ? import_dayjs4.default.utc(value, dim.format, true) : void 0;
49579
- const date2 = formatted?.isValid() ? formatted : import_dayjs4.default.utc(value);
50716
+ const date2 = this._parseDate(value, dim.format);
49580
50717
  Affirm_default(date2.isValid(), `Dimension "${dim.name}" synth "${prop}" ("${value}") is not a valid date${dim.format ? ` for format "${dim.format}" or as an ISO date` : ""}`);
49581
50718
  return date2.valueOf();
49582
50719
  };
@@ -49588,11 +50725,43 @@ var init_SyntheticInputEngine = __esm({
49588
50725
  const synth = this._synth(dim);
49589
50726
  Affirm_default(!(synth.allowedValues && synth.codeSet), `Dimension "${dim.name}" cannot set both synth "allowedValues" and "codeSet"`);
49590
50727
  Affirm_default(!(synth.references && this._hasValueSource(dim)), `Dimension "${dim.name}" cannot combine synth "references" with "allowedValues"/"codeSet"`);
49591
- if (synth.allowedValues) return { enumValues: synth.allowedValues };
50728
+ const type = dim.type ?? "string";
50729
+ if (synth.allowedValues) {
50730
+ Affirm_default(this._valuesFitType(synth.allowedValues, type, dim.format), `Dimension "${dim.name}" declares synth "allowedValues" that are not valid "${type}" values (${JSON.stringify(synth.allowedValues.slice(0, 3))}\u2026)`);
50731
+ return { enumValues: synth.allowedValues };
50732
+ }
49592
50733
  const ref = synth.codeSet ?? matchBuiltinBinding(name);
49593
50734
  if (!ref || synth.references) return {};
49594
- return this._resolveRef(ref, codeSetMap, dim.name);
50735
+ const resolved = this._resolveRef(ref, codeSetMap, dim.name);
50736
+ if (this._valuesFitType(this._setValues(resolved, codeSetMap), type, dim.format)) return resolved;
50737
+ Affirm_default(!synth.codeSet, `Dimension "${dim.name}" is bound to code set "${this._refName(ref)}", whose values are not valid "${type}" values`);
50738
+ return {};
49595
50739
  };
50740
+ // Every value a resolved binding can emit: the flat set's own values, or the bound field's column
50741
+ // across the structured set's records.
50742
+ this._setValues = (resolved, codeSetMap) => resolved.enumValues ?? codeSetMap.get(resolved.codeSet.name.toLowerCase()).records.map((rec) => rec[resolved.codeSet.field]);
50743
+ /**
50744
+ * Whether a closed set of values survives the cast to the dimension's declared type. Enum and code
50745
+ * set values are emitted verbatim and cast when the synthetic file is read back, so a set that
50746
+ * can't produce that type fills the column with garbage — HEDIS codes cast to a `number` are all
50747
+ * NaN, which serializes as null — rather than failing anywhere visible.
50748
+ */
50749
+ this._valuesFitType = (values, type, format4) => {
50750
+ if (type === "string") return true;
50751
+ if (type === "boolean") return values.every((value) => typeof value === "boolean");
50752
+ if (type === "number") return values.every((value) => this._isNumeric(value));
50753
+ return values.every((value) => this._parseDate(value, format4).isValid());
50754
+ };
50755
+ this._isNumeric = (value) => {
50756
+ if (typeof value === "number") return Number.isFinite(value);
50757
+ return typeof value === "string" && value.trim() !== "" && !isNaN(Number(value));
50758
+ };
50759
+ // Read the same way a bound is read: the dimension's own format first, a plain/ISO date otherwise.
50760
+ this._parseDate = (value, format4) => {
50761
+ const formatted = format4 && typeof value === "string" ? import_dayjs4.default.utc(value, format4, true) : void 0;
50762
+ return formatted?.isValid() ? formatted : import_dayjs4.default.utc(value);
50763
+ };
50764
+ this._refName = (ref) => typeof ref === "string" ? ref : ref.name;
49596
50765
  this._resolveRef = (ref, codeSetMap, dimName) => {
49597
50766
  if (typeof ref === "string") {
49598
50767
  const set2 = codeSetMap.get(ref.toLowerCase());
@@ -50240,33 +51409,33 @@ var init_RequestValidator = __esm({
50240
51409
  this.resolveMembers = (consumer) => {
50241
51410
  return new Set(ConsumerManager_default.getExpandedFields(consumer).map((x2) => x2.finalKey));
50242
51411
  };
50243
- this.checkFilters = (value, context, path21, depth) => {
51412
+ this.checkFilters = (value, context, path20, depth) => {
50244
51413
  const { MAX_REQUEST_FILTERS, MAX_REQUEST_FILTER_DEPTH } = Constants_default.defaults;
50245
51414
  if (!Array.isArray(value))
50246
- throw new Error(`Invalid "${path21}": expected an array of filters, got ${this.describe(value)}`);
51415
+ throw new Error(`Invalid "${path20}": expected an array of filters, got ${this.describe(value)}`);
50247
51416
  if (value.length > MAX_REQUEST_FILTERS)
50248
- throw new Error(`Invalid "${path21}": ${value.length} filters is above the maximum of ${MAX_REQUEST_FILTERS}`);
51417
+ throw new Error(`Invalid "${path20}": ${value.length} filters is above the maximum of ${MAX_REQUEST_FILTERS}`);
50249
51418
  if (depth > MAX_REQUEST_FILTER_DEPTH)
50250
- throw new Error(`Invalid filter at ${path21}: filters may not nest more than ${MAX_REQUEST_FILTER_DEPTH} deep`);
50251
- return value.map((x2, index) => this.checkFilter(x2, context, `${path21}[${index}]`, depth));
51419
+ throw new Error(`Invalid filter at ${path20}: filters may not nest more than ${MAX_REQUEST_FILTER_DEPTH} deep`);
51420
+ return value.map((x2, index) => this.checkFilter(x2, context, `${path20}[${index}]`, depth));
50252
51421
  };
50253
- this.checkFilter = (value, context, path21, depth) => {
51422
+ this.checkFilter = (value, context, path20, depth) => {
50254
51423
  if (!value || typeof value !== "object" || Array.isArray(value))
50255
- throw new Error(`Invalid filter at ${path21}: expected an object, got ${this.describe(value)}`);
51424
+ throw new Error(`Invalid filter at ${path20}: expected an object, got ${this.describe(value)}`);
50256
51425
  const filter = value;
50257
51426
  const unknownKey = Object.keys(filter).find((key) => !FILTER_KEYS.includes(key));
50258
51427
  if (unknownKey)
50259
- throw new Error(`Invalid filter at ${path21}: unknown property "${unknownKey}". Supported: ${FILTER_KEYS.join(", ")}`);
50260
- const operator = this.checkOperator(filter.operator, path21);
51428
+ throw new Error(`Invalid filter at ${path20}: unknown property "${unknownKey}". Supported: ${FILTER_KEYS.join(", ")}`);
51429
+ const operator = this.checkOperator(filter.operator, path20);
50261
51430
  const checked = {
50262
- member: this.checkMember(filter.member, context, path21),
51431
+ member: this.checkMember(filter.member, context, path20),
50263
51432
  operator,
50264
- values: this.checkValues(filter.values, operator, path21)
51433
+ values: this.checkValues(filter.values, operator, path20)
50265
51434
  };
50266
51435
  if (Algo_default.hasVal(filter.and))
50267
- checked.and = this.checkFilters(filter.and, context, `${path21}.and`, depth + 1);
51436
+ checked.and = this.checkFilters(filter.and, context, `${path20}.and`, depth + 1);
50268
51437
  if (Algo_default.hasVal(filter.or))
50269
- checked.or = this.checkFilters(filter.or, context, `${path21}.or`, depth + 1);
51438
+ checked.or = this.checkFilters(filter.or, context, `${path20}.or`, depth + 1);
50270
51439
  return checked;
50271
51440
  };
50272
51441
  /**
@@ -50274,18 +51443,18 @@ var init_RequestValidator = __esm({
50274
51443
  * own field names by the time filters run, so any other name reads `undefined` and quietly keeps
50275
51444
  * every row (or drops every row, for a negated operator).
50276
51445
  */
50277
- this.checkMember = (value, context, path21) => {
51446
+ this.checkMember = (value, context, path20) => {
50278
51447
  if (typeof value !== "string" || value.length === 0)
50279
- throw new Error(`Invalid filter at ${path21}: "member" must be a non-empty string, got ${this.describe(value)}`);
51448
+ throw new Error(`Invalid filter at ${path20}: "member" must be a non-empty string, got ${this.describe(value)}`);
50280
51449
  if (!context.members.has(value))
50281
- throw new Error(`Invalid filter at ${path21}: consumer "${context.consumer.name}" has no field named "${value}"`);
51450
+ throw new Error(`Invalid filter at ${path20}: consumer "${context.consumer.name}" has no field named "${value}"`);
50282
51451
  return value;
50283
51452
  };
50284
- this.checkOperator = (value, path21) => {
51453
+ this.checkOperator = (value, path20) => {
50285
51454
  if (typeof value !== "string" || !VALUES_BY_OPERATOR.has(value))
50286
- throw new Error(`Invalid filter at ${path21}: unknown operator ${this.describe(value)}. Supported: ${this.supportedOperators()}`);
51455
+ throw new Error(`Invalid filter at ${path20}: unknown operator ${this.describe(value)}. Supported: ${this.supportedOperators()}`);
50287
51456
  if (VALUES_BY_OPERATOR.get(value) === "unsupported")
50288
- throw new Error(`Invalid filter at ${path21}: operator "${value}" is not supported by a run`);
51457
+ throw new Error(`Invalid filter at ${path20}: operator "${value}" is not supported by a run`);
50289
51458
  return value;
50290
51459
  };
50291
51460
  /**
@@ -50293,24 +51462,24 @@ var init_RequestValidator = __esm({
50293
51462
  * own type and the filter's is read back into it, so a number or a boolean here is a client bug
50294
51463
  * that reads as a type mismatch (matching nothing) rather than as the comparison it meant.
50295
51464
  */
50296
- this.checkValues = (value, operator, path21) => {
51465
+ this.checkValues = (value, operator, path20) => {
50297
51466
  const values = Algo_default.hasVal(value) ? value : [];
50298
51467
  if (!Array.isArray(values))
50299
- throw new Error(`Invalid filter at ${path21}: "values" must be an array of strings, got ${this.describe(value)}`);
51468
+ throw new Error(`Invalid filter at ${path20}: "values" must be an array of strings, got ${this.describe(value)}`);
50300
51469
  const wrongIndex = values.findIndex((x2) => typeof x2 !== "string");
50301
51470
  if (wrongIndex >= 0)
50302
- throw new Error(`Invalid filter at ${path21}: "values[${wrongIndex}]" must be a string, got ${this.describe(values[wrongIndex])}`);
50303
- return this.checkArity(values, operator, path21);
51471
+ throw new Error(`Invalid filter at ${path20}: "values[${wrongIndex}]" must be a string, got ${this.describe(values[wrongIndex])}`);
51472
+ return this.checkArity(values, operator, path20);
50304
51473
  };
50305
- this.checkArity = (values, operator, path21) => {
51474
+ this.checkArity = (values, operator, path20) => {
50306
51475
  const expected = VALUES_BY_OPERATOR.get(operator);
50307
51476
  const reason = `operator "${operator}"`;
50308
51477
  if (expected === "none" && values.length > 0)
50309
- throw new Error(`Invalid filter at ${path21}: ${reason} reads no values, ${values.length} given`);
51478
+ throw new Error(`Invalid filter at ${path20}: ${reason} reads no values, ${values.length} given`);
50310
51479
  if (expected === "many" && values.length === 0)
50311
- throw new Error(`Invalid filter at ${path21}: ${reason} needs at least one value, none given`);
51480
+ throw new Error(`Invalid filter at ${path20}: ${reason} needs at least one value, none given`);
50312
51481
  if (typeof expected === "number" && values.length !== expected)
50313
- throw new Error(`Invalid filter at ${path21}: ${reason} needs exactly ${expected} ${expected === 1 ? "value" : "values"}, ${values.length} given`);
51482
+ throw new Error(`Invalid filter at ${path20}: ${reason} needs exactly ${expected} ${expected === 1 ? "value" : "values"}, ${values.length} given`);
50314
51483
  return values;
50315
51484
  };
50316
51485
  /** Every operator a request may ask for, in the order they are declared. */
@@ -50651,23 +51820,23 @@ var init_Diagnostics = __esm({
50651
51820
  return [.../* @__PURE__ */ new Set([...inputs, ...chained])];
50652
51821
  };
50653
51822
  this._walk = (start, next, onCycle) => {
50654
- const path21 = [];
51823
+ const path20 = [];
50655
51824
  const onPath = /* @__PURE__ */ new Set();
50656
51825
  const stack = [{ name: start, done: false }];
50657
51826
  const visited = /* @__PURE__ */ new Set();
50658
51827
  while (stack.length > 0) {
50659
51828
  const frame = stack.pop();
50660
51829
  if (frame.done) {
50661
- onPath.delete(path21.pop());
51830
+ onPath.delete(path20.pop());
50662
51831
  continue;
50663
51832
  }
50664
51833
  if (onPath.has(frame.name)) {
50665
- path21.slice(path21.indexOf(frame.name)).forEach((name) => onCycle.add(name));
51834
+ path20.slice(path20.indexOf(frame.name)).forEach((name) => onCycle.add(name));
50666
51835
  continue;
50667
51836
  }
50668
51837
  if (visited.has(frame.name)) continue;
50669
51838
  visited.add(frame.name);
50670
- path21.push(frame.name);
51839
+ path20.push(frame.name);
50671
51840
  onPath.add(frame.name);
50672
51841
  stack.push({ name: frame.name, done: true });
50673
51842
  const successors = next.get(frame.name) ?? [];
@@ -51189,7 +52358,7 @@ var init_ResourceView = __esm({
51189
52358
  description: producer.description,
51190
52359
  summary: [
51191
52360
  { label: "source", value: producer.source ?? null, link: producer.source ? LineageEngine_default.nodeId("source", producer.source) : void 0 },
51192
- { label: "reads", value: settings.sqlTable ?? settings.sql ?? settings.fileKey ?? null },
52361
+ { label: "reads", value: settings.sqlTable ?? settings.fileKey ?? null },
51193
52362
  { label: "type", value: settings.fileType ?? "SQL" },
51194
52363
  { label: "dimensions", value: dimensions.length },
51195
52364
  { label: "primary keys", value: dimensions.filter((x2) => x2.pk).length }
@@ -51225,7 +52394,9 @@ var init_ResourceView = __esm({
51225
52394
  { value: this._text(dimension.type) },
51226
52395
  positioned ? { value: this._position(dimension.position) } : { value: this._text(dimension.alias), muted: !dimension.alias },
51227
52396
  { value: !!dimension.pk, muted: !dimension.pk },
51228
- { value: !!dimension.nullable, muted: !dimension.nullable },
52397
+ // Absent means nullable, so an undeclared dimension reads true here rather than false;
52398
+ // muted says the value was defaulted and not written down.
52399
+ { value: dimension.nullable !== false, muted: dimension.nullable === void 0 },
51229
52400
  { value: this._text(dimension.mask), muted: !dimension.mask },
51230
52401
  { value: this._text(dimension.classification), muted: !dimension.classification }
51231
52402
  ]);
@@ -51271,7 +52442,7 @@ var init_ResourceView = __esm({
51271
52442
  { label: "fields", value: (consumer.fields ?? []).length },
51272
52443
  { label: "schema", value: consumer.schema ?? null, link: consumer.schema ? LineageEngine_default.nodeId("schema", consumer.schema) : void 0 },
51273
52444
  { label: "distinct", value: !!consumer.options?.distinct },
51274
- { label: "max file size (Gb)", value: consumer.maximumFileSize ?? null }
52445
+ { label: "split", value: this._splitSummary(consumer) }
51275
52446
  ],
51276
52447
  tables: [
51277
52448
  this._consumerInputsTable(env2, consumer),
@@ -51283,6 +52454,19 @@ var init_ResourceView = __esm({
51283
52454
  raw: this._raw(consumer)
51284
52455
  };
51285
52456
  };
52457
+ /**
52458
+ * How the consumer splits its output, in one line: the key(s) it buckets on, the per-file cap,
52459
+ * or both. `maximumFileSize` is the older spelling of the cap and reads the same here.
52460
+ */
52461
+ this._splitSummary = (consumer) => {
52462
+ const keys = consumer.split?.by ?? [];
52463
+ const maxFileSizeGb = consumer.split?.maxFileSizeGb ?? consumer.maximumFileSize;
52464
+ const parts = [
52465
+ keys.length > 0 ? `by ${keys.join(", ")}` : null,
52466
+ maxFileSizeGb ? `max ${maxFileSizeGb} Gb per file` : null
52467
+ ].filter((x2) => !!x2);
52468
+ return parts.length > 0 ? parts.join(", ") : null;
52469
+ };
51286
52470
  this._consumerInputsTable = (env2, consumer) => {
51287
52471
  const rows = (consumer.producers ?? []).map((input) => [
51288
52472
  { value: input.name, link: this._inputLink(env2, input.name), status: this._inputLink(env2, input.name) ? void 0 : "error" },
@@ -51345,8 +52529,8 @@ var init_ResourceView = __esm({
51345
52529
  if (!source || origin.path.length < 2) return { value: "declared here", muted: true };
51346
52530
  return { value: `${source.resource}.${source.field}`, link: source.node };
51347
52531
  };
51348
- this._pathText = (path21) => {
51349
- return path21.map((step) => `${step.resource}.${step.field}`).join(" \u2192 ");
52532
+ this._pathText = (path20) => {
52533
+ return path20.map((step) => `${step.resource}.${step.field}`).join(" \u2192 ");
51350
52534
  };
51351
52535
  this._consumerOutputsTable = (env2, consumer) => {
51352
52536
  const declared = new Set((env2.sources ?? []).map((x2) => x2.name));
@@ -52173,11 +53357,11 @@ var init_OpsHeartbeat = __esm({
52173
53357
  });
52174
53358
 
52175
53359
  // ../../packages/engines/src/usage/LocalUsageDB.ts
52176
- var import_fs15, import_path16, COLUMNS, CLI_DB_FOLDER, CLI_DB_FILE, CLI_DB_PATH, MAX_USAGE_ROWS, MAX_LISTED_ROWS, EXECUTED_BY_SCHEMA, EXECUTOR_OPERATION_SCHEMA, STATS_SCHEMA, COLUMN_SCHEMAS, LocalUsageDBClass, LocalUsageDB, LocalUsageDB_default;
53360
+ var import_fs17, import_path16, COLUMNS, CLI_DB_FOLDER, CLI_DB_FILE, CLI_DB_PATH, MAX_USAGE_ROWS, MAX_LISTED_ROWS, EXECUTED_BY_SCHEMA, EXECUTOR_OPERATION_SCHEMA, STATS_SCHEMA, COLUMN_SCHEMAS, LocalUsageDBClass, LocalUsageDB, LocalUsageDB_default;
52177
53361
  var init_LocalUsageDB = __esm({
52178
53362
  "../../packages/engines/src/usage/LocalUsageDB.ts"() {
52179
53363
  init_src();
52180
- import_fs15 = require("fs");
53364
+ import_fs17 = require("fs");
52181
53365
  import_path16 = require("path");
52182
53366
  COLUMNS = [
52183
53367
  "_id",
@@ -52368,21 +53552,21 @@ var init_LocalUsageDB = __esm({
52368
53552
  return usage;
52369
53553
  };
52370
53554
  this._getRows = () => {
52371
- const lines = (0, import_fs15.readFileSync)(CLI_DB_PATH, "utf8").split("\n").filter(Boolean);
53555
+ const lines = (0, import_fs17.readFileSync)(CLI_DB_PATH, "utf8").split("\n").filter(Boolean);
52372
53556
  lines.shift();
52373
53557
  return lines;
52374
53558
  };
52375
53559
  this._writeRows = (rows) => {
52376
53560
  const trimmedRows = rows.slice(-MAX_USAGE_ROWS);
52377
- (0, import_fs15.writeFileSync)(CLI_DB_PATH, `${[COLUMNS.join(","), ...trimmedRows].join("\n")}
53561
+ (0, import_fs17.writeFileSync)(CLI_DB_PATH, `${[COLUMNS.join(","), ...trimmedRows].join("\n")}
52378
53562
  `);
52379
53563
  };
52380
53564
  this.init = () => {
52381
53565
  try {
52382
- if (!(0, import_fs15.existsSync)(CLI_DB_FOLDER))
52383
- (0, import_fs15.mkdirSync)(CLI_DB_FOLDER, { recursive: true });
52384
- if (!(0, import_fs15.existsSync)(CLI_DB_PATH))
52385
- (0, import_fs15.writeFileSync)(CLI_DB_PATH, `${COLUMNS.join(",")}
53566
+ if (!(0, import_fs17.existsSync)(CLI_DB_FOLDER))
53567
+ (0, import_fs17.mkdirSync)(CLI_DB_FOLDER, { recursive: true });
53568
+ if (!(0, import_fs17.existsSync)(CLI_DB_PATH))
53569
+ (0, import_fs17.writeFileSync)(CLI_DB_PATH, `${COLUMNS.join(",")}
52386
53570
  `);
52387
53571
  this._writeRows(this._getRows());
52388
53572
  } catch (error) {
@@ -52412,7 +53596,7 @@ var init_LocalUsageDB = __esm({
52412
53596
  */
52413
53597
  this.list = (limit = MAX_LISTED_ROWS, keep) => {
52414
53598
  try {
52415
- if (!(0, import_fs15.existsSync)(CLI_DB_PATH))
53599
+ if (!(0, import_fs17.existsSync)(CLI_DB_PATH))
52416
53600
  return [];
52417
53601
  const rows = this._getRows();
52418
53602
  const wanted = Math.max(1, limit);
@@ -52433,9 +53617,9 @@ var init_LocalUsageDB = __esm({
52433
53617
  */
52434
53618
  this.signature = () => {
52435
53619
  try {
52436
- if (!(0, import_fs15.existsSync)(CLI_DB_PATH))
53620
+ if (!(0, import_fs17.existsSync)(CLI_DB_PATH))
52437
53621
  return "";
52438
- const stats = (0, import_fs15.statSync)(CLI_DB_PATH);
53622
+ const stats = (0, import_fs17.statSync)(CLI_DB_PATH);
52439
53623
  return `${stats.mtimeMs}:${stats.size}`;
52440
53624
  } catch (error) {
52441
53625
  this._handleError("signature", error);
@@ -52602,10 +53786,10 @@ var init_UsageManager = __esm({
52602
53786
  });
52603
53787
 
52604
53788
  // ../../packages/engines/src/ops/RunOverlay.ts
52605
- var import_crypto5, MAX_RUNS, MAX_OPERATIONS, RunOverlayClass, RunOverlay;
53789
+ var import_crypto7, MAX_RUNS, MAX_OPERATIONS, RunOverlayClass, RunOverlay;
52606
53790
  var init_RunOverlay = __esm({
52607
53791
  "../../packages/engines/src/ops/RunOverlay.ts"() {
52608
- import_crypto5 = __toESM(require("crypto"), 1);
53792
+ import_crypto7 = __toESM(require("crypto"), 1);
52609
53793
  init_src();
52610
53794
  init_LineageEngine();
52611
53795
  init_LocalUsageDB();
@@ -52659,7 +53843,7 @@ var init_RunOverlay = __esm({
52659
53843
  * finish action writes runs of its own, so a narrower token would miss a transition the graph
52660
53844
  * exists to show.
52661
53845
  */
52662
- this._token = (overlay) => import_crypto5.default.createHash("sha256").update(JSON.stringify(overlay)).digest("hex");
53846
+ this._token = (overlay) => import_crypto7.default.createHash("sha256").update(JSON.stringify(overlay)).digest("hex");
52663
53847
  this._run = (stat) => {
52664
53848
  const startedAt = this._iso(stat.startedAt);
52665
53849
  return {
@@ -52941,12 +54125,12 @@ var init_LookupRegistry = __esm({
52941
54125
  });
52942
54126
 
52943
54127
  // ../../packages/engines/src/transform/LookupKey.ts
52944
- var import_crypto6, DELIMITER, idFor, LookupKey, LookupKey_default;
54128
+ var import_crypto8, DELIMITER, idFor, LookupKey, LookupKey_default;
52945
54129
  var init_LookupKey = __esm({
52946
54130
  "../../packages/engines/src/transform/LookupKey.ts"() {
52947
- import_crypto6 = __toESM(require("crypto"), 1);
54131
+ import_crypto8 = __toESM(require("crypto"), 1);
52948
54132
  DELIMITER = String.fromCharCode(0);
52949
- idFor = (ref) => import_crypto6.default.createHash("sha1").update([ref.consumer, ref.key, ref.value].join(DELIMITER)).digest("hex").slice(0, 16);
54133
+ idFor = (ref) => import_crypto8.default.createHash("sha1").update([ref.consumer, ref.key, ref.value].join(DELIMITER)).digest("hex").slice(0, 16);
52950
54134
  LookupKey = { idFor };
52951
54135
  LookupKey_default = LookupKey;
52952
54136
  }
@@ -53581,6 +54765,7 @@ var init_src10 = __esm({
53581
54765
  init_RunOverlay();
53582
54766
  init_RunRegistry();
53583
54767
  init_LineParser();
54768
+ init_DiscoverMerger();
53584
54769
  init_ProducerEngine();
53585
54770
  init_ProducerManager();
53586
54771
  init_PushdownEngine();
@@ -53596,16 +54781,14 @@ var init_src10 = __esm({
53596
54781
  });
53597
54782
 
53598
54783
  // ../../packages/executors/src/OutputExecutor.ts
53599
- var fs19, import_path17, OutputExecutorClass, OutputExecutor, OutputExecutor_default;
54784
+ var OutputExecutorClass, OutputExecutor, OutputExecutor_default;
53600
54785
  var init_OutputExecutor = __esm({
53601
54786
  "../../packages/executors/src/OutputExecutor.ts"() {
53602
54787
  init_src();
53603
- fs19 = __toESM(require("fs"));
53604
54788
  init_src();
53605
54789
  init_src8();
53606
54790
  init_src4();
53607
54791
  init_src5();
53608
- import_path17 = __toESM(require("path"));
53609
54792
  init_src2();
53610
54793
  OutputExecutorClass = class {
53611
54794
  constructor() {
@@ -53667,24 +54850,33 @@ var init_OutputExecutor = __esm({
53667
54850
  }
53668
54851
  return JSON.stringify(record);
53669
54852
  };
53670
- this.getCompletedPath = (pathName, fileNumber) => {
53671
- const pathAsArray = pathName.split(".");
53672
- const extension = pathAsArray[1];
53673
- return pathAsArray[0].concat(`_${fileNumber}.${extension}`);
54853
+ /**
54854
+ * The exported name of one split file: the destination name with the file's label before the
54855
+ * extension, e.g. "sales.csv" + "italy_1" -> "sales_italy_1.csv". The extension is taken from
54856
+ * the LAST dot, since a label (and an exportName) may carry one of its own.
54857
+ */
54858
+ this.getCompletedPath = (pathName, label) => {
54859
+ const dot = pathName.lastIndexOf(".");
54860
+ if (dot < 0) return `${pathName}_${label}`;
54861
+ return `${pathName.substring(0, dot)}_${label}${pathName.substring(dot)}`;
53674
54862
  };
53675
- this.exportResult = async (consumer, fields, scope) => {
54863
+ /**
54864
+ * Writes the run's result to every configured destination, one call per file the split pass
54865
+ * produced. The files are passed in rather than read back out of the scope folder: the label a
54866
+ * file carries is what names it at the destination, and with no split configured that list is
54867
+ * the merged dataset alone, labelled "0" — the suffix an unsplit export has always had.
54868
+ */
54869
+ this.exportResult = async (consumer, fields, splitFiles) => {
54870
+ Affirm_default.hasItems(splitFiles, `Consumer "${consumer.name}" has no output file to export`);
53676
54871
  const internalFormat = this._getInternalRecordFormat(consumer);
53677
54872
  const results = [];
53678
54873
  for (const output of consumer.outputs) {
53679
54874
  const destination = Environment_default.getSource(output.exportDestination);
53680
54875
  const driver = await DriverFactory_default.instantiateDestination(destination);
53681
- const currentPath = import_path17.default.dirname(ExecutorScope_default.getMainPath(scope));
53682
54876
  const destinationName = this._composeFileName(consumer, output, this._getExtension(output));
53683
- Logger_default.info(`Exporting consumer "${consumer.name}" to "${output.exportDestination}" as ${output.format} (${destinationName})`);
53684
- const filenameArray = fs19.readdirSync(currentPath).filter((filename) => filename.includes(".dataset"));
53685
- for (const filename in filenameArray) {
53686
- const destinationPath = this.getCompletedPath(destinationName, filename);
53687
- const startingPath = import_path17.default.join(currentPath, filenameArray[filename]);
54877
+ Logger_default.info(`Exporting consumer "${consumer.name}" to "${output.exportDestination}" as ${output.format} (${destinationName}, ${splitFiles.length} file(s))`);
54878
+ for (const { path: startingPath, label } of splitFiles) {
54879
+ const destinationPath = this.getCompletedPath(destinationName, label);
53688
54880
  if (output.format === internalFormat) {
53689
54881
  results.push(await driver.move(startingPath, destinationPath));
53690
54882
  } else {
@@ -53728,9 +54920,18 @@ var init_OutputExecutor = __esm({
53728
54920
  return this._unifingResult(results);
53729
54921
  }
53730
54922
  };
54923
+ /**
54924
+ * Folds the per-file upload results into the one the run reports.
54925
+ *
54926
+ * `every`, not `some`: a destination write that failed is data that never arrived, and one
54927
+ * successful file must not be able to report the run as exported. The drivers catch their own
54928
+ * errors and answer `res: false`, so this flag is the only thing that carries a failed write
54929
+ * back up — `launch` refuses the run on it.
54930
+ */
53731
54931
  this._unifingResult = (resultList) => {
54932
+ Affirm_default.hasItems(resultList, "Export produced no result to report");
53732
54933
  resultList[0].key = resultList.map((res) => res.key).join(", ");
53733
- resultList[0].res = resultList.map((res) => res.res).some((res) => res);
54934
+ resultList[0].res = resultList.every((res) => res.res);
53734
54935
  return resultList[0];
53735
54936
  };
53736
54937
  this._getExtension = (output) => {
@@ -53753,14 +54954,13 @@ var init_OutputExecutor = __esm({
53753
54954
  });
53754
54955
 
53755
54956
  // ../../packages/executors/src/ConsumerExecutor.ts
53756
- var import_path18, import_fs16, import_readline6, import_promises14, import_crypto7, LIMIT_SCAN_BYTES, NEWLINE_BYTE, ConsumerExecutorClass, ConsumerExecutor, ConsumerExecutor_default;
54957
+ var import_path17, import_fs18, import_promises16, import_crypto9, LIMIT_SCAN_BYTES, NEWLINE_BYTE, ConsumerExecutorClass, ConsumerExecutor, ConsumerExecutor_default;
53757
54958
  var init_ConsumerExecutor = __esm({
53758
54959
  "../../packages/executors/src/ConsumerExecutor.ts"() {
53759
- import_path18 = __toESM(require("path"));
53760
- import_fs16 = __toESM(require("fs"));
53761
- import_readline6 = __toESM(require("readline"));
53762
- import_promises14 = __toESM(require("fs/promises"));
53763
- import_crypto7 = __toESM(require("crypto"));
54960
+ import_path17 = __toESM(require("path"));
54961
+ import_fs18 = __toESM(require("fs"));
54962
+ import_promises16 = __toESM(require("fs/promises"));
54963
+ import_crypto9 = __toESM(require("crypto"));
53764
54964
  init_src10();
53765
54965
  init_src10();
53766
54966
  init_src10();
@@ -53774,11 +54974,11 @@ var init_ConsumerExecutor = __esm({
53774
54974
  ConsumerExecutorClass = class {
53775
54975
  constructor() {
53776
54976
  this._ensurePath = (pathUri) => {
53777
- const dir = import_path18.default.dirname(pathUri);
53778
- if (!import_fs16.default.existsSync(dir))
53779
- import_fs16.default.mkdirSync(dir, { recursive: true });
53780
- if (!import_fs16.default.existsSync(pathUri))
53781
- import_fs16.default.writeFileSync(pathUri, "");
54977
+ const dir = import_path17.default.dirname(pathUri);
54978
+ if (!import_fs18.default.existsSync(dir))
54979
+ import_fs18.default.mkdirSync(dir, { recursive: true });
54980
+ if (!import_fs18.default.existsSync(pathUri))
54981
+ import_fs18.default.writeFileSync(pathUri, "");
53782
54982
  };
53783
54983
  /**
53784
54984
  * Shapes one parsed producer record into one consumer record, following a plan compiled once per
@@ -53899,9 +55099,8 @@ var init_ConsumerExecutor = __esm({
53899
55099
  if (lineCountFields.length === 0)
53900
55100
  return 0;
53901
55101
  const tempWorkPath = datasetPath + "_tmp";
53902
- const reader = import_fs16.default.createReadStream(datasetPath);
53903
- const lineReader = import_readline6.default.createInterface({ input: reader, crlfDelay: Infinity });
53904
- const writer = import_fs16.default.createWriteStream(tempWorkPath);
55102
+ const reader = import_fs18.default.createReadStream(datasetPath);
55103
+ const writer = import_fs18.default.createWriteStream(tempWorkPath);
53905
55104
  const waitForDrain = () => new Promise((resolve) => writer.once("drain", resolve));
53906
55105
  let workerIndex = 0;
53907
55106
  let currentWorker = executorResults[workerIndex] ?? null;
@@ -53915,7 +55114,7 @@ var init_ConsumerExecutor = __esm({
53915
55114
  }
53916
55115
  };
53917
55116
  advanceWorker();
53918
- for await (const line of lineReader) {
55117
+ for await (const line of readLines(reader)) {
53919
55118
  advanceWorker();
53920
55119
  Affirm_default(currentWorker, `Unable to resolve worker metadata while processing lineCount for ${datasetPath}`);
53921
55120
  const rowOffset = currentWorker.rowOffset ?? 0;
@@ -53934,7 +55133,6 @@ var init_ConsumerExecutor = __esm({
53934
55133
  remainingLinesInWorker--;
53935
55134
  processedLineCount++;
53936
55135
  }
53937
- lineReader.close();
53938
55136
  await new Promise((resolve, reject) => {
53939
55137
  writer.on("close", resolve);
53940
55138
  writer.on("error", reject);
@@ -53946,8 +55144,8 @@ var init_ConsumerExecutor = __esm({
53946
55144
  reader.destroy();
53947
55145
  });
53948
55146
  }
53949
- await import_promises14.default.unlink(datasetPath);
53950
- await import_promises14.default.rename(tempWorkPath, datasetPath);
55147
+ await import_promises16.default.unlink(datasetPath);
55148
+ await import_promises16.default.rename(tempWorkPath, datasetPath);
53951
55149
  return processedLineCount;
53952
55150
  };
53953
55151
  /**
@@ -53965,7 +55163,7 @@ var init_ConsumerExecutor = __esm({
53965
55163
  this.processLimit = async (datasetPath, limit) => {
53966
55164
  Affirm_default(datasetPath, "Invalid dataset path");
53967
55165
  Affirm_default(limit > 0, `Invalid record limit ${limit}`);
53968
- const handle = await import_promises14.default.open(datasetPath, "r+");
55166
+ const handle = await import_promises16.default.open(datasetPath, "r+");
53969
55167
  try {
53970
55168
  const buffer = Buffer.allocUnsafe(LIMIT_SCAN_BYTES);
53971
55169
  let newlines = 0;
@@ -53991,15 +55189,14 @@ var init_ConsumerExecutor = __esm({
53991
55189
  }
53992
55190
  };
53993
55191
  this.processDistinct = async (datasetPath) => {
53994
- const reader = import_fs16.default.createReadStream(datasetPath);
53995
- const lineReader = import_readline6.default.createInterface({ input: reader, crlfDelay: Infinity });
55192
+ const reader = import_fs18.default.createReadStream(datasetPath);
53996
55193
  const tempWorkPath = datasetPath + "_tmp";
53997
- const writer = import_fs16.default.createWriteStream(tempWorkPath);
55194
+ const writer = import_fs18.default.createWriteStream(tempWorkPath);
53998
55195
  const waitForDrain = () => new Promise((resolve) => writer.once("drain", resolve));
53999
55196
  let newLineCount = 0;
54000
55197
  const seen = /* @__PURE__ */ new Set();
54001
- for await (const line of lineReader) {
54002
- const hash = import_crypto7.default.createHash("sha1").update(line).digest("base64");
55198
+ for await (const line of readLines(reader)) {
55199
+ const hash = import_crypto9.default.createHash("sha1").update(line).digest("base64");
54003
55200
  if (!seen.has(hash)) {
54004
55201
  seen.add(hash);
54005
55202
  if (!writer.write(line + "\n"))
@@ -54007,7 +55204,6 @@ var init_ConsumerExecutor = __esm({
54007
55204
  newLineCount++;
54008
55205
  }
54009
55206
  }
54010
- lineReader.close();
54011
55207
  await new Promise((resolve, reject) => {
54012
55208
  writer.on("close", resolve);
54013
55209
  writer.on("error", reject);
@@ -54019,20 +55215,19 @@ var init_ConsumerExecutor = __esm({
54019
55215
  reader.destroy();
54020
55216
  });
54021
55217
  }
54022
- await import_promises14.default.unlink(datasetPath);
54023
- await import_promises14.default.rename(tempWorkPath, datasetPath);
55218
+ await import_promises16.default.unlink(datasetPath);
55219
+ await import_promises16.default.rename(tempWorkPath, datasetPath);
54024
55220
  return newLineCount;
54025
55221
  };
54026
55222
  this.processDistinctOn = async (consumer, datasetPath) => {
54027
- const reader = import_fs16.default.createReadStream(datasetPath);
54028
- const lineReader = import_readline6.default.createInterface({ input: reader, crlfDelay: Infinity });
55223
+ const reader = import_fs18.default.createReadStream(datasetPath);
54029
55224
  const { distinctOn } = consumer.options;
54030
55225
  const { keys, resolution } = distinctOn;
54031
55226
  const { strategy, orderBy, direction = "asc" } = resolution;
54032
55227
  const internalRecordFormat = OutputExecutor_default._getInternalRecordFormat(consumer);
54033
55228
  const internalFields = ConsumerManager_default.getExpandedFields(consumer);
54034
55229
  const winners = /* @__PURE__ */ new Map();
54035
- for await (const line of lineReader) {
55230
+ for await (const line of readLines(reader)) {
54036
55231
  const record = internalRecordFormat === "CSV" ? LineParser_default._internalParseCSV(line, internalFields) : LineParser_default._internalParseJSON(line);
54037
55232
  const compositeKey = keys.map((k7) => String(record[k7] ?? "")).join("|");
54038
55233
  const existing = winners.get(compositeKey);
@@ -54045,9 +55240,8 @@ var init_ConsumerExecutor = __esm({
54045
55240
  winners.set(compositeKey, { record, line });
54046
55241
  }
54047
55242
  }
54048
- lineReader.close();
54049
55243
  const tempWorkPath = datasetPath + "_tmp";
54050
- const writer = import_fs16.default.createWriteStream(tempWorkPath);
55244
+ const writer = import_fs18.default.createWriteStream(tempWorkPath);
54051
55245
  const waitForDrain = () => new Promise((resolve) => writer.once("drain", resolve));
54052
55246
  for (const { line } of winners.values()) {
54053
55247
  if (!writer.write(line + "\n"))
@@ -54064,8 +55258,8 @@ var init_ConsumerExecutor = __esm({
54064
55258
  reader.destroy();
54065
55259
  });
54066
55260
  }
54067
- await import_promises14.default.unlink(datasetPath);
54068
- await import_promises14.default.rename(tempWorkPath, datasetPath);
55261
+ await import_promises16.default.unlink(datasetPath);
55262
+ await import_promises16.default.rename(tempWorkPath, datasetPath);
54069
55263
  return winners.size;
54070
55264
  };
54071
55265
  this.processPivot = async (consumer, datasetPath) => {
@@ -54077,9 +55271,8 @@ var init_ConsumerExecutor = __esm({
54077
55271
  if (!pivotValues) {
54078
55272
  pivotValues = [];
54079
55273
  const discoverySet = /* @__PURE__ */ new Set();
54080
- const discoverReader = import_fs16.default.createReadStream(datasetPath);
54081
- const discoverLineReader = import_readline6.default.createInterface({ input: discoverReader, crlfDelay: Infinity });
54082
- for await (const line of discoverLineReader) {
55274
+ const discoverReader = import_fs18.default.createReadStream(datasetPath);
55275
+ for await (const line of readLines(discoverReader)) {
54083
55276
  const record = this._parseLine(line, internalRecordFormat, internalFields);
54084
55277
  const val = String(record[pivotColumn] ?? "");
54085
55278
  if (!discoverySet.has(val)) {
@@ -54087,7 +55280,6 @@ var init_ConsumerExecutor = __esm({
54087
55280
  pivotValues.push(val);
54088
55281
  }
54089
55282
  }
54090
- discoverLineReader.close();
54091
55283
  if (!discoverReader.destroyed) {
54092
55284
  await new Promise((resolve) => {
54093
55285
  discoverReader.once("close", resolve);
@@ -54096,9 +55288,8 @@ var init_ConsumerExecutor = __esm({
54096
55288
  }
54097
55289
  }
54098
55290
  const groups = /* @__PURE__ */ new Map();
54099
- const reader = import_fs16.default.createReadStream(datasetPath);
54100
- const lineReader = import_readline6.default.createInterface({ input: reader, crlfDelay: Infinity });
54101
- for await (const line of lineReader) {
55291
+ const reader = import_fs18.default.createReadStream(datasetPath);
55292
+ for await (const line of readLines(reader)) {
54102
55293
  const record = this._parseLine(line, internalRecordFormat, internalFields);
54103
55294
  const compositeKey = rowKeys.map((k7) => String(record[k7] ?? "")).join("|");
54104
55295
  const pivotVal = String(record[pivotColumn] ?? "");
@@ -54118,14 +55309,13 @@ var init_ConsumerExecutor = __esm({
54118
55309
  cell.min = Math.min(cell.min, numericVal);
54119
55310
  cell.max = Math.max(cell.max, numericVal);
54120
55311
  }
54121
- lineReader.close();
54122
55312
  const pivotedFields = [
54123
55313
  ...rowKeys.map((k7) => ({ cField: { key: k7 }, finalKey: k7 })),
54124
55314
  ...pivotValues.map((pv) => ({ cField: { key: columnPrefix + pv }, finalKey: columnPrefix + pv }))
54125
55315
  ];
54126
55316
  const outputPlan = OutputExecutor_default.compileOutput(consumer, pivotedFields);
54127
55317
  const tempWorkPath = datasetPath + "_tmp";
54128
- const writer = import_fs16.default.createWriteStream(tempWorkPath);
55318
+ const writer = import_fs18.default.createWriteStream(tempWorkPath);
54129
55319
  const waitForDrain = () => new Promise((resolve) => writer.once("drain", resolve));
54130
55320
  let outputCount = 0;
54131
55321
  for (const { rowRecord, cells } of groups.values()) {
@@ -54171,8 +55361,8 @@ var init_ConsumerExecutor = __esm({
54171
55361
  reader.destroy();
54172
55362
  });
54173
55363
  }
54174
- await import_promises14.default.unlink(datasetPath);
54175
- await import_promises14.default.rename(tempWorkPath, datasetPath);
55364
+ await import_promises16.default.unlink(datasetPath);
55365
+ await import_promises16.default.rename(tempWorkPath, datasetPath);
54176
55366
  return outputCount;
54177
55367
  };
54178
55368
  this._parseLine = (line, format4, fields) => {
@@ -54218,9 +55408,8 @@ var init_ConsumerExecutor = __esm({
54218
55408
  for (const fieldKey of uniqueFieldKeys) {
54219
55409
  fieldValueSets.set(fieldKey, /* @__PURE__ */ new Set());
54220
55410
  }
54221
- const reader = import_fs16.default.createReadStream(datasetPath);
54222
- const lineReader = import_readline6.default.createInterface({ input: reader, crlfDelay: Infinity });
54223
- for await (const line of lineReader) {
55411
+ const reader = import_fs18.default.createReadStream(datasetPath);
55412
+ for await (const line of readLines(reader)) {
54224
55413
  rowCount++;
54225
55414
  if (checkDuplicateRows) {
54226
55415
  if (seenRows.has(line))
@@ -54242,7 +55431,6 @@ var init_ConsumerExecutor = __esm({
54242
55431
  }
54243
55432
  }
54244
55433
  }
54245
- lineReader.close();
54246
55434
  return DataValidationEngine_default.evaluateDatasetValidations(validations, { rowCount, hasDuplicateRows, duplicateFields });
54247
55435
  };
54248
55436
  /**
@@ -54268,7 +55456,7 @@ var init_ConsumerExecutor = __esm({
54268
55456
  });
54269
55457
 
54270
55458
  // ../../packages/executors/src/ProducerExecutor.ts
54271
- var import_path19, ProducerExecutorClass, ProducerExecutor, ProducerExecutor_default;
55459
+ var import_path18, ProducerExecutorClass, ProducerExecutor, ProducerExecutor_default;
54272
55460
  var init_ProducerExecutor = __esm({
54273
55461
  "../../packages/executors/src/ProducerExecutor.ts"() {
54274
55462
  init_src();
@@ -54276,7 +55464,7 @@ var init_ProducerExecutor = __esm({
54276
55464
  init_src4();
54277
55465
  init_src10();
54278
55466
  init_src10();
54279
- import_path19 = __toESM(require("path"));
55467
+ import_path18 = __toESM(require("path"));
54280
55468
  init_src2();
54281
55469
  ProducerExecutorClass = class {
54282
55470
  constructor() {
@@ -54311,7 +55499,7 @@ var init_ProducerExecutor = __esm({
54311
55499
  this.compileLine = (dimensions, chunk) => {
54312
55500
  Affirm_default(dimensions, "Invalid producer dimensions");
54313
55501
  Affirm_default(chunk, "Invalid chunk");
54314
- return { fills: ProducerManager_default.compileFills(dimensions), sourceFilename: import_path19.default.basename(chunk.fileUri) };
55502
+ return { fills: ProducerManager_default.compileFills(dimensions), sourceFilename: import_path18.default.basename(chunk.fileUri) };
54315
55503
  };
54316
55504
  /**
54317
55505
  * The record(s) one source line produces. Almost always exactly one — a producer with a group
@@ -54367,102 +55555,224 @@ var init_ExecutorPerformance = __esm({
54367
55555
  });
54368
55556
 
54369
55557
  // ../../packages/executors/src/ExecutorWriter.ts
54370
- var fs22, import_readline8, ExecutorWriter, ExecutorWriter_default;
55558
+ var fs23, ExecutorWriter, ExecutorWriter_default;
54371
55559
  var init_ExecutorWriter = __esm({
54372
55560
  "../../packages/executors/src/ExecutorWriter.ts"() {
54373
- fs22 = __toESM(require("fs"));
54374
- import_readline8 = __toESM(require("readline"));
55561
+ fs23 = __toESM(require("fs"));
55562
+ init_src();
55563
+ init_src3();
55564
+ init_src10();
55565
+ init_OutputExecutor();
54375
55566
  ExecutorWriter = class {
54376
55567
  constructor() {
54377
55568
  /**
54378
- * Magic number used to retrive value in gigabytes
54379
- */
54380
- this.FAKE_GB = 1e3 * 1e3 * 1e3;
54381
- /**
54382
- * Retrive the NumberedPath for multifile purpose example: ./main1.dataset
54383
- */
54384
- this.getCompletedPath = (startingPath, count) => {
54385
- const finalPath = startingPath.substring(0, startingPath.length - 8).concat("_", count.toString(), ".dataset");
54386
- return finalPath;
54387
- };
54388
- this.splitBySize = async (scope, sourcePath) => {
54389
- const maxOutputFileSize = scope.limitFileSize * this.FAKE_GB;
54390
- const readStream = fs22.createReadStream(sourcePath);
54391
- const reader = import_readline8.default.createInterface({ input: readStream, crlfDelay: Infinity });
54392
- let writerIndex = 0;
54393
- let destPath = this.getCompletedPath(sourcePath, writerIndex);
54394
- let writeStream = fs22.createWriteStream(destPath, { flags: "a" });
54395
- const waitForDrain = () => new Promise((resolve) => writeStream.once("drain", resolve));
54396
- for await (const line of reader) {
54397
- if (readStream.bytesRead > maxOutputFileSize * (writerIndex + 1)) {
54398
- writeStream.end();
54399
- await new Promise((resolve, reject) => {
54400
- writeStream.on("finish", resolve);
54401
- writeStream.on("error", reject);
54402
- });
54403
- writerIndex++;
54404
- destPath = this.getCompletedPath(sourcePath, writerIndex);
54405
- writeStream = fs22.createWriteStream(destPath, { flags: "a" });
55569
+ * Gigabytes as the configuration means them — 10^9, not 2^30. `maximumFileSize` has always been
55570
+ * read this way and the two differ by 7%, so it stays.
55571
+ */
55572
+ this.BYTES_PER_GB = 1e3 * 1e3 * 1e3;
55573
+ /** The label an unsplit output carries, kept at "0" so exported names do not change. */
55574
+ this.UNSPLIT_LABEL = "0";
55575
+ /** The path of one split file: `main.dataset` plus a label, e.g. `main_italy_1.dataset`. */
55576
+ this.getCompletedPath = (startingPath, label) => {
55577
+ return startingPath.substring(0, startingPath.length - ".dataset".length).concat("_", label, ".dataset");
55578
+ };
55579
+ /**
55580
+ * Routes the merged dataset into one file per bucket, in a single pass.
55581
+ *
55582
+ * Splitting by key and splitting by size are the same operation — decide a line's bucket, append
55583
+ * to that bucket's current file, start the next one when it is full — so one pass covers all
55584
+ * three configurations: a size cap alone (one bucket that rotates), key(s) alone (a bucket each,
55585
+ * never rotating), or both (a bucket each, each rotating on its own counter).
55586
+ *
55587
+ * Returns the files it produced, so the export names them from their label rather than
55588
+ * rediscovering them in the scope folder. With nothing to split the merged dataset is handed
55589
+ * back as-is and never read.
55590
+ */
55591
+ this.splitOutput = async (request) => {
55592
+ Affirm_default(request, "Invalid split request");
55593
+ Affirm_default(request.sourcePath, "Missing dataset to split");
55594
+ Affirm_default(request.consumer, "Invalid consumer");
55595
+ const { scope, sourcePath, consumer, fields } = request;
55596
+ const keys = scope.split?.by ?? [];
55597
+ const maxFileBytes = scope.split?.maxFileSizeGb > 0 ? scope.split.maxFileSizeGb * this.BYTES_PER_GB : null;
55598
+ if (keys.length === 0 && !maxFileBytes)
55599
+ return [{ path: sourcePath, label: this.UNSPLIT_LABEL }];
55600
+ const pass = {
55601
+ consumer,
55602
+ sourcePath,
55603
+ keys,
55604
+ maxFileBytes,
55605
+ buckets: /* @__PURE__ */ new Map(),
55606
+ openStreams: [],
55607
+ usedLabels: /* @__PURE__ */ new Set(),
55608
+ fileCount: 0
55609
+ };
55610
+ await this._route(pass, fields);
55611
+ if (pass.buckets.size === 0)
55612
+ return [{ path: sourcePath, label: this.UNSPLIT_LABEL }];
55613
+ await fs23.promises.unlink(sourcePath);
55614
+ return this._listFiles(pass);
55615
+ };
55616
+ /**
55617
+ * The pass itself: one line read, one line written, for every line of the dataset.
55618
+ */
55619
+ this._route = async (pass, fields) => {
55620
+ const internalFormat = OutputExecutor_default._getInternalRecordFormat(pass.consumer);
55621
+ const readStream = fs23.createReadStream(pass.sourcePath);
55622
+ try {
55623
+ for await (const line of readLines(readStream)) {
55624
+ const bucket = this._bucketFor(line, pass, internalFormat, fields);
55625
+ const lineBytes = Buffer.byteLength(line) + 1;
55626
+ if (this._needsRotation(bucket, pass, lineBytes))
55627
+ await this._rotate(bucket, pass);
55628
+ const stream = await this._acquireStream(bucket, pass);
55629
+ if (!stream.write(line + "\n"))
55630
+ await new Promise((resolve) => stream.once("drain", resolve));
55631
+ bucket.bytesWritten += lineBytes;
54406
55632
  }
54407
- if (!writeStream.write(line + "\n"))
54408
- await waitForDrain();
55633
+ } finally {
55634
+ while (pass.openStreams.length > 0)
55635
+ await this._closeStream(pass.openStreams.shift());
55636
+ readStream.destroy();
54409
55637
  }
54410
- writeStream.end();
55638
+ };
55639
+ /**
55640
+ * The bucket a line belongs to, created on first sight of its key. Null and empty each get their
55641
+ * own: a shared catch-all would silently merge two different things, and that is invisible until
55642
+ * it is wrong.
55643
+ */
55644
+ this._bucketFor = (line, pass, internalFormat, fields) => {
55645
+ const bucketKey = pass.keys.length === 0 ? "" : this._bucketKey(line, pass.keys, internalFormat, fields);
55646
+ const existing = pass.buckets.get(bucketKey);
55647
+ if (existing) return existing;
55648
+ const bucket = {
55649
+ label: pass.keys.length === 0 ? "" : this._uniqueLabel(bucketKey, pass.usedLabels),
55650
+ stream: null,
55651
+ bytesWritten: 0,
55652
+ fileIndex: 0
55653
+ };
55654
+ pass.buckets.set(bucketKey, bucket);
55655
+ this._countFile(pass);
55656
+ return bucket;
55657
+ };
55658
+ /** The composite value a line buckets on, read off the parsed record. */
55659
+ this._bucketKey = (line, keys, internalFormat, fields) => {
55660
+ const record = internalFormat === "CSV" ? LineParser_default._internalParseCSV(line, fields) : LineParser_default._internalParseJSON(line);
55661
+ return keys.map((key) => {
55662
+ const value = record[key];
55663
+ if (value === null || value === void 0) return "_null";
55664
+ const text = String(value);
55665
+ return text.length === 0 ? "_empty" : text;
55666
+ }).join("|");
55667
+ };
55668
+ /**
55669
+ * A key value turned into a file name segment. Anything that is not a letter, a digit, an
55670
+ * underscore or a dash becomes an underscore — dots included, since the export composes names
55671
+ * around the extension — and the result is capped so a long value cannot overrun the path limit.
55672
+ *
55673
+ * Two different values can sanitize to the same segment ("a/b" and "a b"), so a collision takes
55674
+ * a numeric suffix: buckets are keyed by the raw value and must never merge into one file.
55675
+ */
55676
+ this._uniqueLabel = (bucketKey, usedLabels) => {
55677
+ const base = bucketKey.replace(/[^\p{L}\p{N}_-]/gu, "_").substring(0, 64) || "_empty";
55678
+ let label = base;
55679
+ let collision = 1;
55680
+ while (usedLabels.has(label)) {
55681
+ label = `${base}_${collision}`;
55682
+ collision++;
55683
+ }
55684
+ usedLabels.add(label);
55685
+ return label;
55686
+ };
55687
+ /**
55688
+ * Whether this line fills the bucket's current file. Asked BEFORE the line is written, so the
55689
+ * cap is a cap and not a threshold the last line of every file steps over. A line larger than
55690
+ * the cap on its own still goes out whole: rotating an empty file only produces an empty one.
55691
+ */
55692
+ this._needsRotation = (bucket, pass, lineBytes) => {
55693
+ if (!pass.maxFileBytes || bucket.bytesWritten === 0) return false;
55694
+ return bucket.bytesWritten + lineBytes > pass.maxFileBytes;
55695
+ };
55696
+ /** Closes the bucket's full file and points it at the next one. */
55697
+ this._rotate = async (bucket, pass) => {
55698
+ await this._releaseStream(bucket, pass.openStreams);
55699
+ bucket.fileIndex++;
55700
+ bucket.bytesWritten = 0;
55701
+ this._countFile(pass);
55702
+ };
55703
+ /** Counts a file as it is opened, so the bound fails the run before the descriptors run out. */
55704
+ this._countFile = (pass) => {
55705
+ pass.fileCount++;
55706
+ Affirm_default(
55707
+ pass.fileCount <= Constants_default.defaults.MAX_SPLIT_FILES,
55708
+ `Consumer "${pass.consumer.name}" splits its output into more than ${Constants_default.defaults.MAX_SPLIT_FILES} files${pass.keys.length ? ` \u2014 "${pass.keys.join(", ")}" has too many distinct values to split on` : ""}.`
55709
+ );
55710
+ };
55711
+ /**
55712
+ * The bucket's open stream, opening it if the LRU has evicted it. Append mode, always: an
55713
+ * evicted bucket reopens onto the file it was already writing, and its byte counter lives in the
55714
+ * bucket rather than the stream so the rotation point survives the eviction.
55715
+ */
55716
+ this._acquireStream = async (bucket, pass) => {
55717
+ const position = pass.openStreams.indexOf(bucket);
55718
+ if (position >= 0) {
55719
+ pass.openStreams.splice(position, 1);
55720
+ pass.openStreams.push(bucket);
55721
+ return bucket.stream;
55722
+ }
55723
+ if (pass.openStreams.length >= Constants_default.defaults.MAX_OPEN_SPLIT_STREAMS)
55724
+ await this._closeStream(pass.openStreams.shift());
55725
+ bucket.stream = fs23.createWriteStream(this.getCompletedPath(pass.sourcePath, this._composeLabel(bucket, pass)), { flags: "a" });
55726
+ pass.openStreams.push(bucket);
55727
+ return bucket.stream;
55728
+ };
55729
+ /**
55730
+ * Closes a bucket's stream and takes it out of the open set, so the set only ever holds buckets
55731
+ * whose stream is live — what lets `_acquireStream` read "in the set" as "already open".
55732
+ */
55733
+ this._releaseStream = async (bucket, openStreams) => {
55734
+ const position = openStreams.indexOf(bucket);
55735
+ if (position >= 0)
55736
+ openStreams.splice(position, 1);
55737
+ await this._closeStream(bucket);
55738
+ };
55739
+ /** Flushes and releases a bucket's descriptor. Awaited: a reopen must not race the flush. */
55740
+ this._closeStream = async (bucket) => {
55741
+ if (!bucket?.stream) return;
55742
+ const stream = bucket.stream;
55743
+ bucket.stream = null;
55744
+ stream.end();
54411
55745
  await new Promise((resolve, reject) => {
54412
- writeStream.on("finish", resolve);
54413
- writeStream.on("error", reject);
55746
+ stream.on("finish", resolve);
55747
+ stream.on("error", reject);
54414
55748
  });
54415
- await fs22.promises.unlink(sourcePath);
54416
- };
54417
- /**
54418
- * Manage the Writestream for main.dataset
54419
- */
54420
- // write = async (reader: fs.ReadStream) => {
54421
- // if (this._writer) {
54422
- // if (this._writer.closed) {
54423
- // this._writer = createWriteStream(this._mainPath)
54424
- // console.log('new stream')
54425
- // }
54426
- // } else {
54427
- // this._writer = createWriteStream(this._mainPath)
54428
- // }
54429
- // // Convert to array of lines
54430
- // const lines = readline.createInterface({ input: reader, crlfDelay: Infinity })
54431
- // for await (let line of lines) {
54432
- // line = line.concat('\n')
54433
- // if (this._writer) {
54434
- // if (this._limitSizeByte) {
54435
- // if (this._bytesWrote + line.length >= this._limitSizeByte) {
54436
- // this._writer.end()
54437
- // this._writer.close()
54438
- // this._currentFileId++
54439
- // const newPathMain = this.getCompletedPath(this._mainPath)
54440
- // this._writer = createWriteStream(newPathMain)
54441
- // // reset the counter
54442
- // this._bytesWrote = line.length
54443
- // this._writer.write(line)
54444
- // } else {
54445
- // this._writer.write(line)
54446
- // // add to the counter the bytes wrote
54447
- // this._bytesWrote = this._bytesWrote + line.length
54448
- // }
54449
- // } else {
54450
- // this._writer.write(line)
54451
- // }
54452
- // } else {
54453
- // // add a new file and write
54454
- // this._writer = createWriteStream(this._mainPath)
54455
- // this._writer.write(line)
54456
- // }
54457
- // }
54458
- // return
54459
- // }
54460
- /**
54461
- * Terminate the last WriteStream.
54462
- */
54463
- this.end = () => {
54464
- this._writer.end();
54465
- this._writer.close();
55749
+ };
55750
+ /**
55751
+ * The label of one of a bucket's files: the sanitized key value, the rotation index, or both
55752
+ * joined with an underscore — so a size split counts "0", "1", "2", a key split reads "italy",
55753
+ * and the two together read "italy_0", "italy_1".
55754
+ */
55755
+ this._composeLabel = (bucket, pass, fileIndex) => {
55756
+ const segments = [
55757
+ pass.keys.length > 0 ? bucket.label : null,
55758
+ pass.maxFileBytes ? String(fileIndex ?? bucket.fileIndex) : null
55759
+ ];
55760
+ return segments.filter((x2) => x2 !== null).join("_");
55761
+ };
55762
+ /**
55763
+ * Every file the pass wrote, in the order the buckets were first seen and then by rotation.
55764
+ * Derived from the counters rather than read back off the disk, so the export never depends on
55765
+ * directory ordering.
55766
+ */
55767
+ this._listFiles = (pass) => {
55768
+ const files = [];
55769
+ for (const bucket of pass.buckets.values()) {
55770
+ for (let fileIndex = 0; fileIndex <= bucket.fileIndex; fileIndex++) {
55771
+ const label = this._composeLabel(bucket, pass, fileIndex);
55772
+ files.push({ path: this.getCompletedPath(pass.sourcePath, label), label });
55773
+ }
55774
+ }
55775
+ return files;
54466
55776
  };
54467
55777
  }
54468
55778
  };
@@ -54519,14 +55829,14 @@ var init_ExecutorProgress2 = __esm({
54519
55829
  });
54520
55830
 
54521
55831
  // ../../packages/executors/src/OrchestratorHelper.ts
54522
- var import_os2, import_path20, OrchestratorHelper, OrchestratorHelper_default;
55832
+ var import_os2, import_path19, OrchestratorHelper, OrchestratorHelper_default;
54523
55833
  var init_OrchestratorHelper = __esm({
54524
55834
  "../../packages/executors/src/OrchestratorHelper.ts"() {
54525
55835
  init_src();
54526
55836
  init_src6();
54527
55837
  import_os2 = __toESM(require("os"));
54528
55838
  init_src4();
54529
- import_path20 = __toESM(require("path"));
55839
+ import_path19 = __toESM(require("path"));
54530
55840
  init_src3();
54531
55841
  OrchestratorHelper = {
54532
55842
  getMemoryUsage: () => {
@@ -54618,14 +55928,14 @@ var init_OrchestratorHelper = __esm({
54618
55928
  getPhysicalWorkerPath: () => {
54619
55929
  const currentDir = __dirname;
54620
55930
  if (ProcessENVManager_default.getEnvVariable("NODE_ENV") === "dev" || ProcessENVManager_default.getEnvVariable("NODE_ENV") === "development")
54621
- return import_path20.default.resolve("./.build/workers");
55931
+ return import_path19.default.resolve("./.build/workers");
54622
55932
  const forcedPath = ProcessENVManager_default.getEnvVariable("REMORA_WORKERS_PATH");
54623
55933
  if (forcedPath && forcedPath.length > 0)
54624
- return import_path20.default.join(__dirname, forcedPath);
55934
+ return import_path19.default.join(__dirname, forcedPath);
54625
55935
  if (!currentDir.includes(".build")) {
54626
- return import_path20.default.join(__dirname, "../workers");
55936
+ return import_path19.default.join(__dirname, "../workers");
54627
55937
  } else {
54628
- return import_path20.default.resolve("./.build/workers");
55938
+ return import_path19.default.resolve("./.build/workers");
54629
55939
  }
54630
55940
  },
54631
55941
  /**
@@ -54660,6 +55970,29 @@ var init_OrchestratorHelper = __esm({
54660
55970
  const ceiling = Number(raw);
54661
55971
  Affirm_default(Number.isInteger(ceiling) && ceiling >= 1, `REMORA_MAX_WORKERS must be a whole number >= 1, got "${raw}"`);
54662
55972
  return ceiling;
55973
+ },
55974
+ /**
55975
+ * The consumer's splitting, normalized for the run: `maximumFileSize` is the older spelling of
55976
+ * `split.maxFileSizeGb` and is folded in here, so nothing downstream of the scope has to know
55977
+ * there are two. Setting both is rejected at validation, not silently preferred.
55978
+ *
55979
+ * Answers undefined when the consumer asks for no splitting, which is what makes the split pass
55980
+ * a no-op rather than a decision the orchestrator has to take.
55981
+ */
55982
+ resolveSplit: (consumer) => {
55983
+ Affirm_default(consumer, "Invalid consumer");
55984
+ const by = consumer.split?.by ?? [];
55985
+ const maxFileSizeGb = consumer.split?.maxFileSizeGb ?? consumer.maximumFileSize;
55986
+ if (by.length === 0 && !(maxFileSizeGb > 0))
55987
+ return void 0;
55988
+ return { by: by.length > 0 ? by : void 0, maxFileSizeGb: maxFileSizeGb > 0 ? maxFileSizeGb : void 0 };
55989
+ },
55990
+ /** How a run's splitting reads in the log: the keys, the cap, or both. */
55991
+ formatSplit: (split2) => {
55992
+ return [
55993
+ split2.by?.length ? `by ${split2.by.join(", ")}` : null,
55994
+ split2.maxFileSizeGb ? `max ${split2.maxFileSizeGb}Gb per file` : null
55995
+ ].filter((x2) => !!x2).join(", ");
54663
55996
  }
54664
55997
  };
54665
55998
  OrchestratorHelper_default = OrchestratorHelper;
@@ -54667,13 +56000,12 @@ var init_OrchestratorHelper = __esm({
54667
56000
  });
54668
56001
 
54669
56002
  // ../../packages/executors/src/LookupResolver.ts
54670
- var import_fs18, import_promises15, import_path21, import_readline9, LookupResolverClass, LookupResolver, LookupResolver_default;
56003
+ var import_fs20, import_promises17, import_path20, LookupResolverClass, LookupResolver, LookupResolver_default;
54671
56004
  var init_LookupResolver = __esm({
54672
56005
  "../../packages/executors/src/LookupResolver.ts"() {
54673
- import_fs18 = __toESM(require("fs"));
54674
- import_promises15 = __toESM(require("fs/promises"));
54675
- import_path21 = __toESM(require("path"));
54676
- import_readline9 = __toESM(require("readline"));
56006
+ import_fs20 = __toESM(require("fs"));
56007
+ import_promises17 = __toESM(require("fs/promises"));
56008
+ import_path20 = __toESM(require("path"));
54677
56009
  init_src();
54678
56010
  init_src3();
54679
56011
  init_src4();
@@ -54712,11 +56044,11 @@ var init_LookupResolver = __esm({
54712
56044
  this.cache.set(id, await this.buildDict(lookupConsumer, capturePath, ref));
54713
56045
  }
54714
56046
  ExecutorScope_default.ensurePath(outPath);
54715
- await import_promises15.default.writeFile(outPath, JSON.stringify(this.cache.get(id)));
54716
- manifest.push({ id, consumer: ref.consumer, key: ref.key, value: ref.value, path: import_path21.default.resolve(outPath) });
56047
+ await import_promises17.default.writeFile(outPath, JSON.stringify(this.cache.get(id)));
56048
+ manifest.push({ id, consumer: ref.consumer, key: ref.key, value: ref.value, path: import_path20.default.resolve(outPath) });
54717
56049
  }
54718
56050
  for (const capturePath of captureByConsumer.values())
54719
- await import_promises15.default.rm(capturePath, { force: true });
56051
+ await import_promises17.default.rm(capturePath, { force: true });
54720
56052
  return manifest;
54721
56053
  };
54722
56054
  // Runs the source consumer through the real pipeline, capturing its merged output before cleanup.
@@ -54742,8 +56074,7 @@ var init_LookupResolver = __esm({
54742
56074
  const dict = {};
54743
56075
  let rowsRead = 0;
54744
56076
  let warnedDup = false;
54745
- const lineStream = import_readline9.default.createInterface({ input: import_fs18.default.createReadStream(capturePath), crlfDelay: Infinity });
54746
- for await (const line of lineStream) {
56077
+ for await (const line of readLines(import_fs20.default.createReadStream(capturePath))) {
54747
56078
  if (!line.length) continue;
54748
56079
  rowsRead++;
54749
56080
  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})`);
@@ -54763,8 +56094,8 @@ var init_LookupResolver = __esm({
54763
56094
  return JSON.parse(line);
54764
56095
  };
54765
56096
  this.getCapturePath = (scope, consumerName) => {
54766
- const dir = import_path21.default.dirname(ExecutorScope_default.getLookupPath(scope, "x"));
54767
- return import_path21.default.join(dir, `${consumerName}.capture`);
56097
+ const dir = import_path20.default.dirname(ExecutorScope_default.getLookupPath(scope, "x"));
56098
+ return import_path20.default.join(dir, `${consumerName}.capture`);
54768
56099
  };
54769
56100
  this.dedupe = (refs) => {
54770
56101
  const byId = /* @__PURE__ */ new Map();
@@ -54786,12 +56117,12 @@ var init_LookupResolver = __esm({
54786
56117
  });
54787
56118
 
54788
56119
  // ../../packages/executors/src/ConsumerInputResolver.ts
54789
- var import_fs19, import_promises16, import_path22, ConsumerInputResolverClass, ConsumerInputResolver, ConsumerInputResolver_default;
56120
+ var import_fs21, import_promises18, import_path21, ConsumerInputResolverClass, ConsumerInputResolver, ConsumerInputResolver_default;
54790
56121
  var init_ConsumerInputResolver = __esm({
54791
56122
  "../../packages/executors/src/ConsumerInputResolver.ts"() {
54792
- import_fs19 = __toESM(require("fs"));
54793
- import_promises16 = __toESM(require("fs/promises"));
54794
- import_path22 = __toESM(require("path"));
56123
+ import_fs21 = __toESM(require("fs"));
56124
+ import_promises18 = __toESM(require("fs/promises"));
56125
+ import_path21 = __toESM(require("path"));
54795
56126
  init_src();
54796
56127
  init_src3();
54797
56128
  init_src4();
@@ -54814,15 +56145,15 @@ var init_ConsumerInputResolver = __esm({
54814
56145
  Affirm_default(stack.length <= Constants_default.defaults.MAX_CONSUMER_INPUT_DEPTH, `consumer-as-input nesting exceeds MAX_CONSUMER_INPUT_DEPTH (${Constants_default.defaults.MAX_CONSUMER_INPUT_DEPTH}): ${stack.join(" -> ")}`);
54815
56146
  for (const name of upstreamNames) {
54816
56147
  const cached = this.cache.get(name);
54817
- if (!cached || !import_fs19.default.existsSync(cached.path)) {
56148
+ if (!cached || !import_fs21.default.existsSync(cached.path)) {
54818
56149
  Affirm_default(!stack.includes(name), `consumer-as-input cycle detected: ${[...stack, name].join(" -> ")}`);
54819
56150
  const upstream = Environment_default.getConsumer(name);
54820
56151
  Affirm_default(upstream, `consumer-as-input references unknown consumer "${name}"`);
54821
56152
  const capturePath = ExecutorScope_default.getConsumerInputPath(scope, name);
54822
56153
  ExecutorScope_default.ensurePath(capturePath);
54823
56154
  await this.runUpstreamConsumer(upstream, capturePath, stack, request);
54824
- const { size } = await import_promises16.default.stat(capturePath);
54825
- this.cache.set(name, { consumer: name, path: import_path22.default.resolve(capturePath), fileSize: size });
56155
+ const { size } = await import_promises18.default.stat(capturePath);
56156
+ this.cache.set(name, { consumer: name, path: import_path21.default.resolve(capturePath), fileSize: size });
54826
56157
  }
54827
56158
  manifest.set(name, this.cache.get(name));
54828
56159
  }
@@ -54884,13 +56215,12 @@ var ExecutorOrchestrator_exports = {};
54884
56215
  __export(ExecutorOrchestrator_exports, {
54885
56216
  default: () => ExecutorOrchestrator_default
54886
56217
  });
54887
- var import_fs20, import_promises17, import_path23, import_readline10, import_workerpool, import_promises18, ExecutorOrchestratorClass, ExecutorOrchestrator, ExecutorOrchestrator_default;
56218
+ var import_fs22, import_promises19, import_path22, import_workerpool, import_promises20, ExecutorOrchestratorClass, ExecutorOrchestrator, ExecutorOrchestrator_default;
54888
56219
  var init_ExecutorOrchestrator = __esm({
54889
56220
  "../../packages/executors/src/ExecutorOrchestrator.ts"() {
54890
- import_fs20 = __toESM(require("fs"));
54891
- import_promises17 = __toESM(require("fs/promises"));
54892
- import_path23 = __toESM(require("path"));
54893
- import_readline10 = __toESM(require("readline"));
56221
+ import_fs22 = __toESM(require("fs"));
56222
+ import_promises19 = __toESM(require("fs/promises"));
56223
+ import_path22 = __toESM(require("path"));
54894
56224
  import_workerpool = __toESM(require("workerpool"));
54895
56225
  init_src();
54896
56226
  init_src10();
@@ -54906,7 +56236,7 @@ var init_ExecutorOrchestrator = __esm({
54906
56236
  init_src();
54907
56237
  init_src10();
54908
56238
  init_ExecutorWriter();
54909
- import_promises18 = require("stream/promises");
56239
+ import_promises20 = require("stream/promises");
54910
56240
  init_src2();
54911
56241
  init_ExecutorProgress2();
54912
56242
  init_src5();
@@ -54945,7 +56275,7 @@ var init_ExecutorOrchestrator = __esm({
54945
56275
  };
54946
56276
  const workerPath = OrchestratorHelper_default.getPhysicalWorkerPath();
54947
56277
  Logger_default.info(`Initializing worker pool from ${workerPath} (workers: ${maxWorkers}, heap limit: ${Constants_default.defaults.MIN_RUNTIME_HEAP_MB}MB) | ${OrchestratorHelper_default.formatMemoryUsage()}`);
54948
- return import_workerpool.default.pool(import_path23.default.join(workerPath, "ExecutorWorker.js"), options2);
56278
+ return import_workerpool.default.pool(import_path22.default.join(workerPath, "ExecutorWorker.js"), options2);
54949
56279
  };
54950
56280
  this.launch = async (request) => {
54951
56281
  Affirm_default(request, "Invalid options");
@@ -54957,7 +56287,7 @@ var init_ExecutorOrchestrator = __esm({
54957
56287
  const tracker = new ExecutorPerformance_default();
54958
56288
  const _progress = new ExecutorProgress2_default(logProgress);
54959
56289
  const { usageId } = UsageManager_default.startUsage(consumer, details);
54960
- const scope = { id: usageId, folder: `${consumer.name}_${usageId}`, workersId: [], limitFileSize: consumer.maximumFileSize };
56290
+ const scope = { id: usageId, folder: `${consumer.name}_${usageId}`, workersId: [], split: OrchestratorHelper_default.resolveSplit(consumer) };
54961
56291
  const expandedFields = ConsumerManager_default.getExpandedFields(consumer);
54962
56292
  const census = { created: 0, peak: 0 };
54963
56293
  try {
@@ -55087,20 +56417,23 @@ var init_ExecutorOrchestrator = __esm({
55087
56417
  await Logger_default.flush();
55088
56418
  return materializeResult;
55089
56419
  }
55090
- if (scope.limitFileSize) {
55091
- Logger_default.log(`[${usageId}] Splitting output by size limit (${scope.limitFileSize})`);
55092
- counter = performance.now();
55093
- const writer = new ExecutorWriter_default();
55094
- await writer.splitBySize(scope, ExecutorScope_default.getMainPath(scope));
55095
- tracker.measure("split-by-size", performance.now() - counter);
55096
- Logger_default.log(`[${usageId}] Split complete in ${Math.round(performance.now() - counter)}ms`);
55097
- }
56420
+ counter = performance.now();
56421
+ const splitFiles = await new ExecutorWriter_default().splitOutput({
56422
+ scope,
56423
+ sourcePath: ExecutorScope_default.getMainPath(scope),
56424
+ consumer,
56425
+ fields: expandedFields
56426
+ });
56427
+ tracker.measure("split-output", performance.now() - counter);
56428
+ if (scope.split)
56429
+ Logger_default.log(`[${usageId}] Split output into ${splitFiles.length} file(s) (${OrchestratorHelper_default.formatSplit(scope.split)}) in ${Math.round(performance.now() - counter)}ms`);
55098
56430
  _progress.update({ phase: "Post-processing", progress: 1 });
55099
56431
  _progress.update({ phase: "Exporting result", progress: 0 });
55100
56432
  counter = performance.now();
55101
56433
  Logger_default.info(`[${usageId}] Exporting results to ${consumer.outputs.length} output(s)`);
55102
- const exportRes = await OutputExecutor_default.exportResult(consumer, expandedFields, scope);
56434
+ const exportRes = await OutputExecutor_default.exportResult(consumer, expandedFields, splitFiles);
55103
56435
  tracker.measure("export-result", performance.now() - counter);
56436
+ 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`);
55104
56437
  Logger_default.info(`[${usageId}] Export complete in ${Math.round(performance.now() - counter)}ms (key: ${exportRes.key})`);
55105
56438
  _progress.update({ phase: "Exporting result", progress: 1 });
55106
56439
  if (consumer.outputs.some((x2) => x2.onSuccess)) {
@@ -55146,7 +56479,7 @@ var init_ExecutorOrchestrator = __esm({
55146
56479
  Logger_default.info(`[${usageId}] Producer "${prod.name}": source returned no files, skipping`);
55147
56480
  continue;
55148
56481
  }
55149
- if (!import_fs20.default.existsSync(response.files[0].fullUri)) {
56482
+ if (!import_fs22.default.existsSync(response.files[0].fullUri)) {
55150
56483
  if (!cProd.isOptional)
55151
56484
  throw new Error(`Expected data file ${response.files[0].fullUri} of producer ${prod.name} in consumer ${consumer.name} is missing.`);
55152
56485
  Logger_default.info(`[${usageId}] Producer "${prod.name}": data file missing but marked optional, skipping`);
@@ -55308,7 +56641,7 @@ var init_ExecutorOrchestrator = __esm({
55308
56641
  * Returns a single chunk for small files where parallelism overhead isn't worth it.
55309
56642
  */
55310
56643
  this.scopeWork = (fileUri, numChunks) => {
55311
- const fileSize = import_fs20.default.statSync(fileUri).size;
56644
+ const fileSize = import_fs22.default.statSync(fileUri).size;
55312
56645
  if (fileSize === 0) return [];
55313
56646
  if (fileSize < Constants_default.defaults.MIN_FILE_SIZE_FOR_PARALLEL) {
55314
56647
  return [{ start: 0, end: fileSize, isFirstChunk: true, fileUri, index: 0 }];
@@ -55318,7 +56651,7 @@ var init_ExecutorOrchestrator = __esm({
55318
56651
  const effectiveChunks = Math.min(targetWorkers, maxChunksBySize);
55319
56652
  if (effectiveChunks <= 1) return [{ start: 0, end: fileSize, isFirstChunk: true, fileUri, index: 0 }];
55320
56653
  const targetChunkSize = Math.floor(fileSize / effectiveChunks);
55321
- const fd = import_fs20.default.openSync(fileUri, "r");
56654
+ const fd = import_fs22.default.openSync(fileUri, "r");
55322
56655
  try {
55323
56656
  const offsets = [];
55324
56657
  let currentStart = 0;
@@ -55336,7 +56669,7 @@ var init_ExecutorOrchestrator = __esm({
55336
56669
  }
55337
56670
  return offsets;
55338
56671
  } finally {
55339
- import_fs20.default.closeSync(fd);
56672
+ import_fs22.default.closeSync(fd);
55340
56673
  }
55341
56674
  };
55342
56675
  /**
@@ -55404,7 +56737,7 @@ var init_ExecutorOrchestrator = __esm({
55404
56737
  let currentPos = position;
55405
56738
  while (currentPos < fileSize) {
55406
56739
  const bytesToRead = Math.min(BUFFER_SIZE, fileSize - currentPos);
55407
- const bytesRead = import_fs20.default.readSync(fd, buffer, 0, bytesToRead, currentPos);
56740
+ const bytesRead = import_fs22.default.readSync(fd, buffer, 0, bytesToRead, currentPos);
55408
56741
  if (bytesRead === 0) break;
55409
56742
  for (let i6 = 0; i6 < bytesRead; i6++) {
55410
56743
  if (buffer[i6] === 10) {
@@ -55423,17 +56756,16 @@ var init_ExecutorOrchestrator = __esm({
55423
56756
  * format has no header row, so each row is reprojected. Without a captureFormat it's a verbatim copy.
55424
56757
  */
55425
56758
  this.captureDataset = async (consumer, fields, sourcePath, targetPath, captureFormat) => {
55426
- await import_promises17.default.mkdir(import_path23.default.dirname(targetPath), { recursive: true });
56759
+ await import_promises19.default.mkdir(import_path22.default.dirname(targetPath), { recursive: true });
55427
56760
  const internalFormat = OutputExecutor_default._getInternalRecordFormat(consumer);
55428
56761
  if (captureFormat !== "JSONL" || internalFormat !== "CSV") {
55429
- await import_promises17.default.copyFile(sourcePath, targetPath);
56762
+ await import_promises19.default.copyFile(sourcePath, targetPath);
55430
56763
  return;
55431
56764
  }
55432
56765
  const visibleFields = fields.filter((field) => !field.cField.hidden);
55433
- const input = import_readline10.default.createInterface({ input: import_fs20.default.createReadStream(sourcePath), crlfDelay: Infinity });
55434
- const output = import_fs20.default.createWriteStream(targetPath);
56766
+ const output = import_fs22.default.createWriteStream(targetPath);
55435
56767
  try {
55436
- for await (const line of input) {
56768
+ for await (const line of readLines(import_fs22.default.createReadStream(sourcePath))) {
55437
56769
  if (!line.length) continue;
55438
56770
  const record = LineParser_default._internalParseCSV(line, visibleFields, ",");
55439
56771
  if (!output.write(JSON.stringify(record) + "\n"))
@@ -55521,7 +56853,7 @@ var init_ExecutorOrchestrator = __esm({
55521
56853
  const decodedFiles = [];
55522
56854
  let decodedCount = 0;
55523
56855
  for (const file of response.files) {
55524
- const inferredType = fileType ?? import_path23.default.extname(file.fullUri).replace(".", "").toUpperCase();
56856
+ const inferredType = fileType ?? import_path22.default.extname(file.fullUri).replace(".", "").toUpperCase();
55525
56857
  const decodedPath = `${file.fullUri}.decoded.csv`;
55526
56858
  if (inferredType === "XLS" || inferredType === "XLSX") {
55527
56859
  const xlsCsvStream = await XLSParser_default.getStreamXls(file.fullUri, {
@@ -55529,22 +56861,22 @@ var init_ExecutorOrchestrator = __esm({
55529
56861
  startRow: prod.settings.startRow,
55530
56862
  startColumn: prod.settings.startColumn
55531
56863
  });
55532
- await (0, import_promises18.pipeline)(
56864
+ await (0, import_promises20.pipeline)(
55533
56865
  xlsCsvStream,
55534
- import_fs20.default.createWriteStream(decodedPath)
56866
+ import_fs22.default.createWriteStream(decodedPath)
55535
56867
  );
55536
- const fileStats = await import_promises17.default.stat(decodedPath);
56868
+ const fileStats = await import_promises19.default.stat(decodedPath);
55537
56869
  decodedFiles.push({ fullUri: decodedPath, fileSize: fileStats.size });
55538
56870
  decodedCount++;
55539
56871
  reportFile();
55540
56872
  continue;
55541
56873
  }
55542
56874
  if (inferredType === "XML") {
55543
- const fileContent = await import_promises17.default.readFile(file.fullUri, "utf-8");
56875
+ const fileContent = await import_promises19.default.readFile(file.fullUri, "utf-8");
55544
56876
  const jsonData = XMLParser_default.xmlToJson(fileContent);
55545
56877
  const records = normalizeXmlRows(jsonData);
55546
56878
  if (records.length === 0) {
55547
- await import_promises17.default.writeFile(decodedPath, "", "utf-8");
56879
+ await import_promises19.default.writeFile(decodedPath, "", "utf-8");
55548
56880
  } else {
55549
56881
  const columns = [];
55550
56882
  for (const record of records) {
@@ -55560,9 +56892,9 @@ var init_ExecutorOrchestrator = __esm({
55560
56892
  const row = columns.map((column) => csvSafeValue(record[column]));
55561
56893
  lines.push(CSVParser_default.stringifyRow(row));
55562
56894
  }
55563
- await import_promises17.default.writeFile(decodedPath, lines.join("\n"), "utf-8");
56895
+ await import_promises19.default.writeFile(decodedPath, lines.join("\n"), "utf-8");
55564
56896
  }
55565
- const fileStats = await import_promises17.default.stat(decodedPath);
56897
+ const fileStats = await import_promises19.default.stat(decodedPath);
55566
56898
  decodedFiles.push({ fullUri: decodedPath, fileSize: fileStats.size });
55567
56899
  decodedCount++;
55568
56900
  reportFile();
@@ -55590,20 +56922,20 @@ var init_ExecutorOrchestrator = __esm({
55590
56922
  Logger_default.log(`[${scope.id}] Merging ${executorResults.length} worker output files into ${mainPath}`);
55591
56923
  const perf = performance.now();
55592
56924
  for (const workerResult of executorResults) {
55593
- await (0, import_promises18.pipeline)(
55594
- import_fs20.default.createReadStream(workerResult.resultUri),
55595
- import_fs20.default.createWriteStream(mainPath, { flags: "a" })
56925
+ await (0, import_promises20.pipeline)(
56926
+ import_fs22.default.createReadStream(workerResult.resultUri),
56927
+ import_fs22.default.createWriteStream(mainPath, { flags: "a" })
55596
56928
  );
55597
- await import_promises17.default.unlink(workerResult.resultUri);
56929
+ await import_promises19.default.unlink(workerResult.resultUri);
55598
56930
  }
55599
56931
  tracker.measure("merge-workers", performance.now() - perf);
55600
56932
  Logger_default.log(`[${scope.id}] Merge complete in ${Math.round(performance.now() - perf)}ms`);
55601
56933
  } else if (executorResults.length === 1) {
55602
56934
  Logger_default.log(`[${scope.id}] Single worker \u2014 renaming output to ${mainPath}`);
55603
- await import_promises17.default.rename(executorResults[0].resultUri, mainPath);
56935
+ await import_promises19.default.rename(executorResults[0].resultUri, mainPath);
55604
56936
  } else {
55605
56937
  Logger_default.log(`[${scope.id}] No worker output \u2014 writing an empty dataset to ${mainPath}`);
55606
- await import_promises17.default.writeFile(mainPath, "");
56938
+ await import_promises19.default.writeFile(mainPath, "");
55607
56939
  }
55608
56940
  return mainPath;
55609
56941
  };
@@ -55625,54 +56957,12 @@ var init_ExecutorOrchestrator = __esm({
55625
56957
  }
55626
56958
  });
55627
56959
 
55628
- // ../../packages/executors/src/BatchWriter.ts
55629
- init_src();
55630
- var FLUSH_AFTER_CHARS = 1 << 18;
55631
- var BatchWriter = class {
55632
- constructor(stream) {
55633
- /** How many times the stream's buffer filled and the caller had to wait for it. */
55634
- this.drains = 0;
55635
- this.buffer = "";
55636
- /** Queues one line and its newline. False means a batch went out and the stream wants a drain. */
55637
- this.write = (line) => {
55638
- this.buffer += line;
55639
- this.buffer += "\n";
55640
- if (this.buffer.length < FLUSH_AFTER_CHARS)
55641
- return true;
55642
- return this.flush();
55643
- };
55644
- this.waitForDrain = () => {
55645
- this.drains++;
55646
- return new Promise((resolve) => this.stream.once("drain", resolve));
55647
- };
55648
- /** Writes whatever is still buffered, then waits for the stream to finish. */
55649
- this.close = async () => {
55650
- this.flush();
55651
- await new Promise((resolve, reject) => {
55652
- this.stream.on("finish", resolve);
55653
- this.stream.on("error", reject);
55654
- this.stream.end();
55655
- });
55656
- };
55657
- this.flush = () => {
55658
- if (!this.buffer)
55659
- return true;
55660
- const batch = this.buffer;
55661
- this.buffer = "";
55662
- return this.stream.write(batch);
55663
- };
55664
- Affirm_default(stream, "Invalid write stream");
55665
- this.stream = stream;
55666
- }
55667
- };
55668
- var BatchWriter_default = BatchWriter;
55669
-
55670
56960
  // ../../packages/executors/src/index.ts
56961
+ init_src();
55671
56962
  init_ConsumerExecutor();
55672
56963
 
55673
56964
  // ../../packages/executors/src/Executor.ts
55674
- var import_fs17 = __toESM(require("fs"));
55675
- var import_readline7 = __toESM(require("readline"));
56965
+ var import_fs19 = __toESM(require("fs"));
55676
56966
  init_ProducerExecutor();
55677
56967
  init_ConsumerExecutor();
55678
56968
 
@@ -55856,10 +57146,9 @@ var Executor = class {
55856
57146
  let processedBytes = 0;
55857
57147
  Logger_default.log(`[${workerId}] Opened streams \u2014 chunk ${start}-${end} (${Math.round(totalBytes / 1024)}KB), ${fields.length} field(s)`);
55858
57148
  Logger_default.log(`[${workerId}] Starting line-by-line processing`);
55859
- const lineStream = import_readline7.default.createInterface({ input: readStream, crlfDelay: Infinity });
55860
57149
  const isDebug = Logger_default._level === "debug";
55861
57150
  try {
55862
- for await (const line of lineStream) {
57151
+ for await (const line of readLines(readStream)) {
55863
57152
  if (lineIndex === 0 && isFirstChunk) {
55864
57153
  if (!this.shouldProcessFirstLine(producer)) {
55865
57154
  lineIndex++;
@@ -55963,18 +57252,18 @@ var Executor = class {
55963
57252
  if (!lookups?.length) return;
55964
57253
  for (const entry of lookups) {
55965
57254
  if (LookupRegistry_default.has(entry.id)) continue;
55966
- const table = JSON.parse(import_fs17.default.readFileSync(entry.path, "utf-8"));
57255
+ const table = JSON.parse(import_fs19.default.readFileSync(entry.path, "utf-8"));
55967
57256
  LookupRegistry_default.set(entry.id, table);
55968
57257
  Logger_default.log(`[lookup] Hydrated table "${entry.consumer}" (${entry.key}->${entry.value}) from ${entry.path}`);
55969
57258
  }
55970
57259
  };
55971
57260
  this.openReadStream = (chunk) => {
55972
57261
  const { end, fileUri, start } = chunk;
55973
- return import_fs17.default.createReadStream(fileUri, { start, end: end - 1 });
57262
+ return import_fs19.default.createReadStream(fileUri, { start, end: end - 1 });
55974
57263
  };
55975
57264
  this.openWriteStream = (scope, workerId) => {
55976
57265
  const workerPath = ExecutorScope_default.getWorkerPath(scope, workerId);
55977
- return new BatchWriter_default(import_fs17.default.createWriteStream(workerPath));
57266
+ return new BatchWriter_default(import_fs19.default.createWriteStream(workerPath));
55978
57267
  };
55979
57268
  // The first line of the first chunk is data unless the file type carries a header there.
55980
57269
  this.shouldProcessFirstLine = (producer) => {
@@ -56080,10 +57369,9 @@ init_OutputExecutor();
56080
57369
  init_ProducerExecutor();
56081
57370
 
56082
57371
  // ../../packages/executors/src/SampleRunner.ts
56083
- var import_fs21 = __toESM(require("fs"));
56084
- var import_promises19 = __toESM(require("fs/promises"));
56085
- var import_path24 = __toESM(require("path"));
56086
- var import_readline11 = __toESM(require("readline"));
57372
+ var import_fs23 = __toESM(require("fs"));
57373
+ var import_promises21 = __toESM(require("fs/promises"));
57374
+ var import_path23 = __toESM(require("path"));
56087
57375
  init_src();
56088
57376
  init_src3();
56089
57377
  init_src4();
@@ -56128,7 +57416,7 @@ var SampleRunnerClass = class {
56128
57416
  */
56129
57417
  this._runConsumer = async (consumer, size, details) => {
56130
57418
  this._assertStageable(consumer);
56131
- const capturePath = import_path24.default.resolve(import_path24.default.join(ExecutorScope_default.getBasePath(), `sample_${consumer.name}_${Helper_default.uuid()}.jsonl`));
57419
+ const capturePath = import_path23.default.resolve(import_path23.default.join(ExecutorScope_default.getBasePath(), `sample_${consumer.name}_${Helper_default.uuid()}.jsonl`));
56132
57420
  ExecutorScope_default.ensurePath(capturePath);
56133
57421
  try {
56134
57422
  const result = await ExecutorOrchestrator_default.launch({
@@ -56155,7 +57443,7 @@ var SampleRunnerClass = class {
56155
57443
  note: `${records.length} row(s) from the real pipeline \u2014 filters, transformations, masking and validation all applied, exactly as in a run. Nothing was written to the destination.`
56156
57444
  };
56157
57445
  } finally {
56158
- await import_promises19.default.rm(capturePath, { force: true });
57446
+ await import_promises21.default.rm(capturePath, { force: true });
56159
57447
  }
56160
57448
  };
56161
57449
  /**
@@ -56218,17 +57506,12 @@ var SampleRunnerClass = class {
56218
57506
  };
56219
57507
  /** The captured dataset, which `captureFormat: 'JSONL'` guarantees is one JSON object per line. */
56220
57508
  this._readCapture = async (capturePath, size) => {
56221
- if (!import_fs21.default.existsSync(capturePath)) return [];
57509
+ if (!import_fs23.default.existsSync(capturePath)) return [];
56222
57510
  const records = [];
56223
- const lines = import_readline11.default.createInterface({ input: import_fs21.default.createReadStream(capturePath), crlfDelay: Infinity });
56224
- try {
56225
- for await (const line of lines) {
56226
- if (!line.length) continue;
56227
- records.push(JSON.parse(line));
56228
- if (records.length >= size) break;
56229
- }
56230
- } finally {
56231
- lines.close();
57511
+ for await (const line of readLines(import_fs23.default.createReadStream(capturePath))) {
57512
+ if (!line.length) continue;
57513
+ records.push(JSON.parse(line));
57514
+ if (records.length >= size) break;
56232
57515
  }
56233
57516
  return records;
56234
57517
  };