@forzalabs/remora 2.5.0 → 2.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +20 -0
- package/docs/Remora - Consumers.md +18 -10
- package/index.js +41 -17
- package/package.json +1 -1
- package/workers/ExecutorWorker.js +41 -17
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,26 @@ The format is based on Keep a Changelog, and this project adheres to Semantic Ve
|
|
|
6
6
|
|
|
7
7
|
## Unreleased
|
|
8
8
|
|
|
9
|
+
## V 2.6.0 - 2026-10-26
|
|
10
|
+
|
|
11
|
+
**Upgrading from 2.5.** Nothing in an existing project changes, and nothing that compiled under 2.5 is refused. One thing moves, and it is not opt-out: a `CSV` or `TXT` export now **quotes a field only where the quotes carry meaning**, where it used to quote every field. The values and the nulls are the same values and the same nulls — `"USR-2","free"` is now `USR-2,free` — but the bytes are not, so a destination that matches the file against a fixture, or parses it by splitting on `","` rather than with a CSV reader, needs checking before this version runs. This is the second consecutive release to move those bytes, after 2.4.0's null marker; a proper CSV reader has been unaffected by both.
|
|
12
|
+
|
|
13
|
+
### Changed
|
|
14
|
+
|
|
15
|
+
- **A positional field is quoted only when it needs to be.** Every field of a `CSV`/`TXT` export and of the internal staged dataset used to be wrapped in quotes unconditionally, which cost two bytes per field per row — written by each worker, concatenated by the merge, re-read by every post-processing pass, and shipped to the destination. A value is now written bare unless it holds the delimiter, a `"`, a line break, or a space at either end, and an **empty string stays quoted** because that is what keeps `""` (the empty string) apart from nothing at all (a null). The header line follows the same rule, so it still parses with the reader the data does.
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
id,note,amount
|
|
19
|
+
USR-1,"",12.5 <- note is the empty string
|
|
20
|
+
USR-2,, <- note and amount are null
|
|
21
|
+
USR-3,"a, b",12.5 <- note holds the delimiter, so it is quoted
|
|
22
|
+
USR-4," padded ",0 <- note has edge spaces, so it is quoted
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
**Measured -14.7% on the bytes** of a real 192MB export (201,795,101 → 172,095,071 over 990,000 records of 15 columns), which comes off every stage that reads or writes a staged file and off what the destination stores. The encode itself is 1.17x faster per row, and end to end the wall clock moves inside the harness's own ±6% noise floor — the bytes are the result here, not the throughput. A staged dataset written by an older version, where everything is quoted, still reads back identically, so nothing in a half-finished run is stranded.
|
|
26
|
+
|
|
27
|
+
- **Edge spaces are quoted deliberately**, which is a rule the original specification did not have. Most CSV readers trim an unquoted field — Remora's own does — so a value of ` padded ` written bare would come back shorter than it went out. Quoting it is what keeps the round trip exact, and it costs nothing measurable: the check rides in the same single pass that looks for the delimiter and the quote.
|
|
28
|
+
|
|
9
29
|
## V 2.5.0 - 2026-10-26
|
|
10
30
|
|
|
11
31
|
**Upgrading from 2.4.** Nothing in an existing project changes. `FIXED` is a new output format and every rule this release adds fires only on a consumer that declares one, so no configuration needs touching, no exported bytes move, and nothing that compiled under 2.4 is refused. The one thing to know before writing a FIXED output is what the receiver has to do with the file: it leaves Remora as UTF-8 text with `lf` or `crlf` line terminators, so a z/OS target still needs the ASCII→EBCDIC and `RECFM=FB` conversion on its own side (see Added).
|
|
@@ -1042,11 +1042,11 @@ Records then legitimately differ in shape from one another, which is why it appl
|
|
|
1042
1042
|
|
|
1043
1043
|
### **The Header Line**
|
|
1044
1044
|
|
|
1045
|
-
A `CSV` or `TXT` file is positional: it carries values and nothing that names them. So both formats write a **header line** first — the exported field names, in column order, quoted and delimited exactly
|
|
1045
|
+
A `CSV` or `TXT` file is positional: it carries values and nothing that names them. So both formats write a **header line** first — the exported field names, in column order, quoted and delimited on exactly the rule the rows under it follow, so the header parses with the same reader the data does:
|
|
1046
1046
|
|
|
1047
1047
|
```
|
|
1048
|
-
|
|
1049
|
-
|
|
1048
|
+
id,primary_email,plan,country
|
|
1049
|
+
USR-00001,alice.rossi@example.com,free,Italy
|
|
1050
1050
|
```
|
|
1051
1051
|
|
|
1052
1052
|
The names are the exported ones: a field's `alias` where it has one, its `key` otherwise. A `hidden` field is not written, so it is not in the header either. With a `split`, **every** file of the output carries the header, not just the first one — each file has to stand on its own.
|
|
@@ -1070,20 +1070,28 @@ It only applies to `CSV` and `TXT`. On a `JSON` or `API` output each record alre
|
|
|
1070
1070
|
|
|
1071
1071
|
### **How a value is written**
|
|
1072
1072
|
|
|
1073
|
-
A `CSV`/`TXT` file
|
|
1074
|
-
|
|
1075
|
-
|
|
1073
|
+
A `CSV`/`TXT` file **quotes a value only where the quotes carry meaning**. A value is written bare
|
|
1074
|
+
unless it holds the delimiter, a `"`, a line break, or a space at either end — and an empty string
|
|
1075
|
+
is quoted too, because that is what leaves the unquoted empty field free to mean something else: it
|
|
1076
|
+
is a **null**. The two are different values, and on a positional line this is the only way to tell
|
|
1077
|
+
them apart:
|
|
1076
1078
|
|
|
1077
1079
|
```
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1080
|
+
id,note,amount
|
|
1081
|
+
USR-1,"",12.5 <- note is the empty string
|
|
1082
|
+
USR-2,, <- note and amount are null
|
|
1083
|
+
USR-3,"a, b",12.5 <- note holds the delimiter, so it is quoted
|
|
1084
|
+
USR-4," padded ",0 <- note has edge spaces, so it is quoted
|
|
1081
1085
|
```
|
|
1082
1086
|
|
|
1083
1087
|
A reader that treats an unquoted empty field as "no value" — which most CSV readers do, and which is
|
|
1084
1088
|
what `COPY`, `LOAD DATA` and a pandas `read_csv` all assume — gets a null back. A reader that does
|
|
1085
1089
|
not sees an empty string, exactly as before.
|
|
1086
1090
|
|
|
1091
|
+
Edge spaces are quoted for the same reason the empty string is: most CSV readers, Remora's own
|
|
1092
|
+
included, trim an unquoted field, so ` padded ` written bare would come back shorter than it went
|
|
1093
|
+
out. Quoting it is what makes the round trip exact.
|
|
1094
|
+
|
|
1087
1095
|
A value holding a **newline** or a carriage return is written as the two characters `\n` or `\r`.
|
|
1088
1096
|
A line of a remora file is one record, everywhere in the pipeline, so a value carrying a real
|
|
1089
1097
|
newline would otherwise arrive as two broken rows. The escape is one-way: nothing turns it back into
|
|
@@ -1307,7 +1315,7 @@ Apache Parquet: a typed, SNAPPY-compressed **columnar** file, which is what a da
|
|
|
1307
1315
|
}
|
|
1308
1316
|
```
|
|
1309
1317
|
|
|
1310
|
-
Two things it gives a reader that a `CSV` export cannot. The file **declares its types**, so nothing downstream has to guess them back out of the text. And it is much smaller: a four-column, 2-million-row dataset is about 28 MB as parquet against
|
|
1318
|
+
Two things it gives a reader that a `CSV` export cannot. The file **declares its types**, so nothing downstream has to guess them back out of the text. And it is much smaller: a four-column, 2-million-row dataset is about 28 MB as parquet against roughly 100 MB as CSV.
|
|
1311
1319
|
|
|
1312
1320
|
The schema comes from what the consumer already declares — each field's dimension type:
|
|
1313
1321
|
|
package/index.js
CHANGED
|
@@ -11761,7 +11761,7 @@ var CONSTANTS, Constants_default;
|
|
|
11761
11761
|
var init_Constants = __esm({
|
|
11762
11762
|
"../../packages/constants/src/Constants.ts"() {
|
|
11763
11763
|
CONSTANTS = {
|
|
11764
|
-
cliVersion: "2.
|
|
11764
|
+
cliVersion: "2.6.0",
|
|
11765
11765
|
backendVersion: 1,
|
|
11766
11766
|
backendPort: 5088,
|
|
11767
11767
|
workerVersion: 2,
|
|
@@ -33962,21 +33962,29 @@ var init_CSVParser = __esm({
|
|
|
33962
33962
|
});
|
|
33963
33963
|
|
|
33964
33964
|
// ../../packages/parsing/src/DatasetCodec.ts
|
|
33965
|
-
var NULL_FIELD, NEEDS_ESCAPE, DatasetCodecClass, DatasetCodec, DatasetCodec_default;
|
|
33965
|
+
var NULL_FIELD, EMPTY_FIELD, NEEDS_ESCAPE, QUOTE_CODE, QUOTE_TESTS, DatasetCodecClass, DatasetCodec, DatasetCodec_default;
|
|
33966
33966
|
var init_DatasetCodec = __esm({
|
|
33967
33967
|
"../../packages/parsing/src/DatasetCodec.ts"() {
|
|
33968
33968
|
init_src();
|
|
33969
33969
|
NULL_FIELD = "";
|
|
33970
|
+
EMPTY_FIELD = '""';
|
|
33970
33971
|
NEEDS_ESCAPE = /["\n\r]/;
|
|
33972
|
+
QUOTE_CODE = 34;
|
|
33973
|
+
QUOTE_TESTS = /* @__PURE__ */ new Map([[",", /^\s|\s$|[",\n\r]/]]);
|
|
33971
33974
|
DatasetCodecClass = class {
|
|
33972
33975
|
constructor() {
|
|
33973
33976
|
/**
|
|
33974
|
-
* One value as an internal positional field
|
|
33975
|
-
*
|
|
33977
|
+
* One value as an internal positional field. Bare where it can be, quoted where it must be -
|
|
33978
|
+
* around the delimiter, a quote, a line break or an edge space - or the unquoted null marker
|
|
33979
|
+
* when there is no value. The delimiter is a parameter rather than a comma because `redelimit`
|
|
33980
|
+
* re-encodes a staged line with the export's own: a tab is safe bare in the staged file and is
|
|
33981
|
+
* not once that file is re-emitted tab-delimited.
|
|
33976
33982
|
*/
|
|
33977
|
-
this.encodeField = (value) => {
|
|
33983
|
+
this.encodeField = (value, delimiter = ",") => {
|
|
33978
33984
|
if (value === null || value === void 0) return NULL_FIELD;
|
|
33979
33985
|
const text = typeof value === "string" ? value : String(value);
|
|
33986
|
+
if (text === "") return EMPTY_FIELD;
|
|
33987
|
+
if (!this._quoteTest(delimiter).test(text)) return text;
|
|
33980
33988
|
if (!NEEDS_ESCAPE.test(text)) return `"${text}"`;
|
|
33981
33989
|
return `"${text.replaceAll('"', '""').replaceAll("\r", "\\r").replaceAll("\n", "\\n")}"`;
|
|
33982
33990
|
};
|
|
@@ -33988,7 +33996,7 @@ var init_DatasetCodec = __esm({
|
|
|
33988
33996
|
for (let index = 0; index < values.length; index++) {
|
|
33989
33997
|
if (index > 0)
|
|
33990
33998
|
row += delimiter;
|
|
33991
|
-
row += this.encodeField(values[index]);
|
|
33999
|
+
row += this.encodeField(values[index], delimiter);
|
|
33992
34000
|
}
|
|
33993
34001
|
return row;
|
|
33994
34002
|
};
|
|
@@ -34008,26 +34016,26 @@ var init_DatasetCodec = __esm({
|
|
|
34008
34016
|
let index = 0;
|
|
34009
34017
|
while (index < length) {
|
|
34010
34018
|
const char = line.charCodeAt(index);
|
|
34011
|
-
if (char ===
|
|
34019
|
+
if (char === QUOTE_CODE) {
|
|
34012
34020
|
if (!inQuotes) {
|
|
34013
34021
|
inQuotes = true;
|
|
34014
34022
|
hasQuotes = true;
|
|
34015
34023
|
fieldStart = index + 1;
|
|
34016
|
-
} else if (line.charCodeAt(index + 1) ===
|
|
34024
|
+
} else if (line.charCodeAt(index + 1) === QUOTE_CODE) {
|
|
34017
34025
|
index++;
|
|
34018
34026
|
} else {
|
|
34019
34027
|
inQuotes = false;
|
|
34020
34028
|
fieldEnd = index;
|
|
34021
34029
|
}
|
|
34022
34030
|
} else if (char === delimiterCode && !inQuotes) {
|
|
34023
|
-
fields.push(this._field(line, fieldStart, fieldEnd, hasQuotes));
|
|
34031
|
+
fields.push(this._field(line, fieldStart, hasQuotes ? fieldEnd : index, hasQuotes));
|
|
34024
34032
|
fieldStart = index + 1;
|
|
34025
34033
|
fieldEnd = 0;
|
|
34026
34034
|
hasQuotes = false;
|
|
34027
34035
|
}
|
|
34028
34036
|
index++;
|
|
34029
34037
|
}
|
|
34030
|
-
fields.push(this._field(line, fieldStart, fieldEnd, hasQuotes));
|
|
34038
|
+
fields.push(this._field(line, fieldStart, hasQuotes ? fieldEnd : length, hasQuotes));
|
|
34031
34039
|
return fields;
|
|
34032
34040
|
};
|
|
34033
34041
|
/**
|
|
@@ -34038,9 +34046,24 @@ var init_DatasetCodec = __esm({
|
|
|
34038
34046
|
Affirm_default(delimiter, "Invalid delimiter");
|
|
34039
34047
|
return this.encodeRow(this.decodeRow(line), delimiter);
|
|
34040
34048
|
};
|
|
34041
|
-
/**
|
|
34049
|
+
/**
|
|
34050
|
+
* The quoting test for one delimiter, built on first use. The four characters escaped into the
|
|
34051
|
+
* class are the ones a character class reads as syntax; anything else stands for itself there.
|
|
34052
|
+
*/
|
|
34053
|
+
this._quoteTest = (delimiter) => {
|
|
34054
|
+
let test = QUOTE_TESTS.get(delimiter);
|
|
34055
|
+
if (!test) {
|
|
34056
|
+
test = new RegExp(`^\\s|\\s$|["\\n\\r${delimiter.replace(/[\\\]^-]/g, "\\$&")}]`);
|
|
34057
|
+
QUOTE_TESTS.set(delimiter, test);
|
|
34058
|
+
}
|
|
34059
|
+
return test;
|
|
34060
|
+
};
|
|
34061
|
+
/**
|
|
34062
|
+
* One field's text, or null for the unquoted empty marker. A bare token carries its value as it
|
|
34063
|
+
* stands - nothing bare can hold a quote, so there is nothing to unescape in it.
|
|
34064
|
+
*/
|
|
34042
34065
|
this._field = (line, start, end, hasQuotes) => {
|
|
34043
|
-
if (!hasQuotes) return null;
|
|
34066
|
+
if (!hasQuotes) return start === end ? null : line.slice(start, end);
|
|
34044
34067
|
const raw = line.slice(start, end);
|
|
34045
34068
|
return raw.indexOf('"') === -1 ? raw : raw.replaceAll('""', '"');
|
|
34046
34069
|
};
|
|
@@ -56695,7 +56718,7 @@ var init_OutputExecutor = __esm({
|
|
|
56695
56718
|
for (let index = 0; index < visibleKeys.length; index++) {
|
|
56696
56719
|
if (index > 0)
|
|
56697
56720
|
row += delimiter;
|
|
56698
|
-
row += DatasetCodec_default.encodeField(record[visibleKeys[index]]);
|
|
56721
|
+
row += DatasetCodec_default.encodeField(record[visibleKeys[index]], delimiter);
|
|
56699
56722
|
}
|
|
56700
56723
|
return row;
|
|
56701
56724
|
};
|
|
@@ -56880,9 +56903,10 @@ var init_OutputExecutor = __esm({
|
|
|
56880
56903
|
return resultList[0];
|
|
56881
56904
|
};
|
|
56882
56905
|
/**
|
|
56883
|
-
* The header line of a positional export: the visible field names, in column order,
|
|
56884
|
-
*
|
|
56885
|
-
* does. Null for a JSON/API output, where a
|
|
56906
|
+
* The header line of a positional export: the visible field names, in column order, written
|
|
56907
|
+
* through the SAME codec as the rows under them — so a name is quoted on the same rule a value
|
|
56908
|
+
* is, and the header parses with the reader the data does. Null for a JSON/API output, where a
|
|
56909
|
+
* record already carries its own field names, for a
|
|
56886
56910
|
* FIXED one, where a name does not fit the column it describes and the receiver reads by
|
|
56887
56911
|
* position anyway, and for an output that turns the header off.
|
|
56888
56912
|
*
|
|
@@ -56891,7 +56915,7 @@ var init_OutputExecutor = __esm({
|
|
|
56891
56915
|
this._composeHeader = (output, dsSchema) => {
|
|
56892
56916
|
if (output.header === false) return null;
|
|
56893
56917
|
if (output.format !== "CSV" && output.format !== "TXT") return null;
|
|
56894
|
-
return
|
|
56918
|
+
return DatasetCodec_default.encodeRow(DatasetSchema_default.names(dsSchema), this._getDelimiter(output));
|
|
56895
56919
|
};
|
|
56896
56920
|
/** The delimiter the exported file is written with. Only TXT configures it; CSV is a comma. */
|
|
56897
56921
|
this._getDelimiter = (output) => {
|
package/package.json
CHANGED
|
@@ -11809,7 +11809,7 @@ var CONSTANTS, Constants_default;
|
|
|
11809
11809
|
var init_Constants = __esm({
|
|
11810
11810
|
"../../packages/constants/src/Constants.ts"() {
|
|
11811
11811
|
CONSTANTS = {
|
|
11812
|
-
cliVersion: "2.
|
|
11812
|
+
cliVersion: "2.6.0",
|
|
11813
11813
|
backendVersion: 1,
|
|
11814
11814
|
backendPort: 5088,
|
|
11815
11815
|
workerVersion: 2,
|
|
@@ -33745,21 +33745,29 @@ var init_CSVParser = __esm({
|
|
|
33745
33745
|
});
|
|
33746
33746
|
|
|
33747
33747
|
// ../../packages/parsing/src/DatasetCodec.ts
|
|
33748
|
-
var NULL_FIELD, NEEDS_ESCAPE, DatasetCodecClass, DatasetCodec, DatasetCodec_default;
|
|
33748
|
+
var NULL_FIELD, EMPTY_FIELD, NEEDS_ESCAPE, QUOTE_CODE, QUOTE_TESTS, DatasetCodecClass, DatasetCodec, DatasetCodec_default;
|
|
33749
33749
|
var init_DatasetCodec = __esm({
|
|
33750
33750
|
"../../packages/parsing/src/DatasetCodec.ts"() {
|
|
33751
33751
|
init_src();
|
|
33752
33752
|
NULL_FIELD = "";
|
|
33753
|
+
EMPTY_FIELD = '""';
|
|
33753
33754
|
NEEDS_ESCAPE = /["\n\r]/;
|
|
33755
|
+
QUOTE_CODE = 34;
|
|
33756
|
+
QUOTE_TESTS = /* @__PURE__ */ new Map([[",", /^\s|\s$|[",\n\r]/]]);
|
|
33754
33757
|
DatasetCodecClass = class {
|
|
33755
33758
|
constructor() {
|
|
33756
33759
|
/**
|
|
33757
|
-
* One value as an internal positional field
|
|
33758
|
-
*
|
|
33760
|
+
* One value as an internal positional field. Bare where it can be, quoted where it must be -
|
|
33761
|
+
* around the delimiter, a quote, a line break or an edge space - or the unquoted null marker
|
|
33762
|
+
* when there is no value. The delimiter is a parameter rather than a comma because `redelimit`
|
|
33763
|
+
* re-encodes a staged line with the export's own: a tab is safe bare in the staged file and is
|
|
33764
|
+
* not once that file is re-emitted tab-delimited.
|
|
33759
33765
|
*/
|
|
33760
|
-
this.encodeField = (value) => {
|
|
33766
|
+
this.encodeField = (value, delimiter = ",") => {
|
|
33761
33767
|
if (value === null || value === void 0) return NULL_FIELD;
|
|
33762
33768
|
const text = typeof value === "string" ? value : String(value);
|
|
33769
|
+
if (text === "") return EMPTY_FIELD;
|
|
33770
|
+
if (!this._quoteTest(delimiter).test(text)) return text;
|
|
33763
33771
|
if (!NEEDS_ESCAPE.test(text)) return `"${text}"`;
|
|
33764
33772
|
return `"${text.replaceAll('"', '""').replaceAll("\r", "\\r").replaceAll("\n", "\\n")}"`;
|
|
33765
33773
|
};
|
|
@@ -33771,7 +33779,7 @@ var init_DatasetCodec = __esm({
|
|
|
33771
33779
|
for (let index = 0; index < values.length; index++) {
|
|
33772
33780
|
if (index > 0)
|
|
33773
33781
|
row += delimiter;
|
|
33774
|
-
row += this.encodeField(values[index]);
|
|
33782
|
+
row += this.encodeField(values[index], delimiter);
|
|
33775
33783
|
}
|
|
33776
33784
|
return row;
|
|
33777
33785
|
};
|
|
@@ -33791,26 +33799,26 @@ var init_DatasetCodec = __esm({
|
|
|
33791
33799
|
let index = 0;
|
|
33792
33800
|
while (index < length) {
|
|
33793
33801
|
const char = line.charCodeAt(index);
|
|
33794
|
-
if (char ===
|
|
33802
|
+
if (char === QUOTE_CODE) {
|
|
33795
33803
|
if (!inQuotes) {
|
|
33796
33804
|
inQuotes = true;
|
|
33797
33805
|
hasQuotes = true;
|
|
33798
33806
|
fieldStart = index + 1;
|
|
33799
|
-
} else if (line.charCodeAt(index + 1) ===
|
|
33807
|
+
} else if (line.charCodeAt(index + 1) === QUOTE_CODE) {
|
|
33800
33808
|
index++;
|
|
33801
33809
|
} else {
|
|
33802
33810
|
inQuotes = false;
|
|
33803
33811
|
fieldEnd = index;
|
|
33804
33812
|
}
|
|
33805
33813
|
} else if (char === delimiterCode && !inQuotes) {
|
|
33806
|
-
fields.push(this._field(line, fieldStart, fieldEnd, hasQuotes));
|
|
33814
|
+
fields.push(this._field(line, fieldStart, hasQuotes ? fieldEnd : index, hasQuotes));
|
|
33807
33815
|
fieldStart = index + 1;
|
|
33808
33816
|
fieldEnd = 0;
|
|
33809
33817
|
hasQuotes = false;
|
|
33810
33818
|
}
|
|
33811
33819
|
index++;
|
|
33812
33820
|
}
|
|
33813
|
-
fields.push(this._field(line, fieldStart, fieldEnd, hasQuotes));
|
|
33821
|
+
fields.push(this._field(line, fieldStart, hasQuotes ? fieldEnd : length, hasQuotes));
|
|
33814
33822
|
return fields;
|
|
33815
33823
|
};
|
|
33816
33824
|
/**
|
|
@@ -33821,9 +33829,24 @@ var init_DatasetCodec = __esm({
|
|
|
33821
33829
|
Affirm_default(delimiter, "Invalid delimiter");
|
|
33822
33830
|
return this.encodeRow(this.decodeRow(line), delimiter);
|
|
33823
33831
|
};
|
|
33824
|
-
/**
|
|
33832
|
+
/**
|
|
33833
|
+
* The quoting test for one delimiter, built on first use. The four characters escaped into the
|
|
33834
|
+
* class are the ones a character class reads as syntax; anything else stands for itself there.
|
|
33835
|
+
*/
|
|
33836
|
+
this._quoteTest = (delimiter) => {
|
|
33837
|
+
let test = QUOTE_TESTS.get(delimiter);
|
|
33838
|
+
if (!test) {
|
|
33839
|
+
test = new RegExp(`^\\s|\\s$|["\\n\\r${delimiter.replace(/[\\\]^-]/g, "\\$&")}]`);
|
|
33840
|
+
QUOTE_TESTS.set(delimiter, test);
|
|
33841
|
+
}
|
|
33842
|
+
return test;
|
|
33843
|
+
};
|
|
33844
|
+
/**
|
|
33845
|
+
* One field's text, or null for the unquoted empty marker. A bare token carries its value as it
|
|
33846
|
+
* stands - nothing bare can hold a quote, so there is nothing to unescape in it.
|
|
33847
|
+
*/
|
|
33825
33848
|
this._field = (line, start, end, hasQuotes) => {
|
|
33826
|
-
if (!hasQuotes) return null;
|
|
33849
|
+
if (!hasQuotes) return start === end ? null : line.slice(start, end);
|
|
33827
33850
|
const raw = line.slice(start, end);
|
|
33828
33851
|
return raw.indexOf('"') === -1 ? raw : raw.replaceAll('""', '"');
|
|
33829
33852
|
};
|
|
@@ -56687,7 +56710,7 @@ var init_OutputExecutor = __esm({
|
|
|
56687
56710
|
for (let index = 0; index < visibleKeys.length; index++) {
|
|
56688
56711
|
if (index > 0)
|
|
56689
56712
|
row += delimiter;
|
|
56690
|
-
row += DatasetCodec_default.encodeField(record[visibleKeys[index]]);
|
|
56713
|
+
row += DatasetCodec_default.encodeField(record[visibleKeys[index]], delimiter);
|
|
56691
56714
|
}
|
|
56692
56715
|
return row;
|
|
56693
56716
|
};
|
|
@@ -56872,9 +56895,10 @@ var init_OutputExecutor = __esm({
|
|
|
56872
56895
|
return resultList[0];
|
|
56873
56896
|
};
|
|
56874
56897
|
/**
|
|
56875
|
-
* The header line of a positional export: the visible field names, in column order,
|
|
56876
|
-
*
|
|
56877
|
-
* does. Null for a JSON/API output, where a
|
|
56898
|
+
* The header line of a positional export: the visible field names, in column order, written
|
|
56899
|
+
* through the SAME codec as the rows under them — so a name is quoted on the same rule a value
|
|
56900
|
+
* is, and the header parses with the reader the data does. Null for a JSON/API output, where a
|
|
56901
|
+
* record already carries its own field names, for a
|
|
56878
56902
|
* FIXED one, where a name does not fit the column it describes and the receiver reads by
|
|
56879
56903
|
* position anyway, and for an output that turns the header off.
|
|
56880
56904
|
*
|
|
@@ -56883,7 +56907,7 @@ var init_OutputExecutor = __esm({
|
|
|
56883
56907
|
this._composeHeader = (output, dsSchema) => {
|
|
56884
56908
|
if (output.header === false) return null;
|
|
56885
56909
|
if (output.format !== "CSV" && output.format !== "TXT") return null;
|
|
56886
|
-
return
|
|
56910
|
+
return DatasetCodec_default.encodeRow(DatasetSchema_default.names(dsSchema), this._getDelimiter(output));
|
|
56887
56911
|
};
|
|
56888
56912
|
/** The delimiter the exported file is written with. Only TXT configures it; CSV is a comma. */
|
|
56889
56913
|
this._getDelimiter = (output) => {
|