rowpipe 1.0.0 → 1.1.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/README.md +25 -32
- package/dist/cli/index.js +4 -4
- package/dist/cli/index.js.map +1 -1
- package/dist/core/types.d.ts +2 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/readers/csv.d.ts.map +1 -1
- package/dist/readers/csv.js +28 -16
- package/dist/readers/csv.js.map +1 -1
- package/dist/readers/index.d.ts +1 -0
- package/dist/readers/index.d.ts.map +1 -1
- package/dist/readers/index.js +29 -3
- package/dist/readers/index.js.map +1 -1
- package/dist/readers/json.d.ts.map +1 -1
- package/dist/readers/json.js +6 -8
- package/dist/readers/json.js.map +1 -1
- package/dist/readers/jsonl.d.ts.map +1 -1
- package/dist/readers/jsonl.js +5 -7
- package/dist/readers/jsonl.js.map +1 -1
- package/dist/readers/parquet.d.ts +17 -0
- package/dist/readers/parquet.d.ts.map +1 -0
- package/dist/readers/parquet.js +146 -0
- package/dist/readers/parquet.js.map +1 -0
- package/dist/utils/compression.d.ts +30 -6
- package/dist/utils/compression.d.ts.map +1 -1
- package/dist/utils/compression.js +95 -18
- package/dist/utils/compression.js.map +1 -1
- package/dist/writers/csv.d.ts.map +1 -1
- package/dist/writers/csv.js +9 -11
- package/dist/writers/csv.js.map +1 -1
- package/dist/writers/index.d.ts +2 -0
- package/dist/writers/index.d.ts.map +1 -1
- package/dist/writers/index.js +49 -4
- package/dist/writers/index.js.map +1 -1
- package/dist/writers/json.d.ts +1 -1
- package/dist/writers/json.d.ts.map +1 -1
- package/dist/writers/json.js +9 -11
- package/dist/writers/json.js.map +1 -1
- package/dist/writers/jsonl.d.ts +1 -1
- package/dist/writers/jsonl.d.ts.map +1 -1
- package/dist/writers/jsonl.js +9 -11
- package/dist/writers/jsonl.js.map +1 -1
- package/dist/writers/markdown.d.ts +21 -0
- package/dist/writers/markdown.d.ts.map +1 -0
- package/dist/writers/markdown.js +82 -0
- package/dist/writers/markdown.js.map +1 -0
- package/dist/writers/parquet.d.ts +19 -0
- package/dist/writers/parquet.d.ts.map +1 -0
- package/dist/writers/parquet.js +129 -0
- package/dist/writers/parquet.js.map +1 -0
- package/package.json +13 -2
- package/skills/rowpipe/SKILL.md +181 -0
- package/benchmarks/memory-bench.ts +0 -109
- package/src/analytics/reduce.ts +0 -388
- package/src/analytics/schema-inference.ts +0 -217
- package/src/analytics/semantic-types.ts +0 -36
- package/src/analytics/stats.ts +0 -383
- package/src/analytics/validator.ts +0 -134
- package/src/cli/commands/cast.ts +0 -60
- package/src/cli/commands/convert.ts +0 -128
- package/src/cli/commands/filter.ts +0 -58
- package/src/cli/commands/inspect.ts +0 -193
- package/src/cli/commands/map.ts +0 -66
- package/src/cli/commands/reduce.ts +0 -74
- package/src/cli/commands/rename.ts +0 -59
- package/src/cli/commands/sample.ts +0 -61
- package/src/cli/commands/schema.ts +0 -86
- package/src/cli/commands/select.ts +0 -59
- package/src/cli/commands/stats.ts +0 -100
- package/src/cli/commands/validate.ts +0 -123
- package/src/cli/index.ts +0 -285
- package/src/core/batch.ts +0 -81
- package/src/core/errors.ts +0 -81
- package/src/core/pipeline.ts +0 -150
- package/src/core/types.ts +0 -110
- package/src/index.ts +0 -52
- package/src/readers/csv.ts +0 -302
- package/src/readers/index.ts +0 -88
- package/src/readers/json.ts +0 -256
- package/src/readers/jsonl.ts +0 -126
- package/src/readers/xlsx.ts +0 -182
- package/src/transforms/cast.ts +0 -212
- package/src/transforms/expression.ts +0 -1114
- package/src/transforms/filter.ts +0 -38
- package/src/transforms/map.ts +0 -99
- package/src/transforms/rename.ts +0 -47
- package/src/transforms/sample.ts +0 -57
- package/src/transforms/select.ts +0 -64
- package/src/utils/compression.ts +0 -43
- package/src/utils/formatting.ts +0 -75
- package/src/utils/progress.ts +0 -37
- package/src/writers/csv.ts +0 -106
- package/src/writers/index.ts +0 -87
- package/src/writers/json.ts +0 -52
- package/src/writers/jsonl.ts +0 -49
- package/src/writers/xlsx.ts +0 -87
- package/tests/analytics.test.ts +0 -119
- package/tests/cli-integration.test.ts +0 -187
- package/tests/map-reduce.test.ts +0 -223
- package/tests/readers-writers.test.ts +0 -189
- package/tests/transforms.test.ts +0 -199
- package/tsconfig.json +0 -21
- package/vitest.config.ts +0 -9
package/src/readers/jsonl.ts
DELETED
|
@@ -1,126 +0,0 @@
|
|
|
1
|
-
import { createReadStream } from "node:fs";
|
|
2
|
-
import { createInterface } from "node:readline";
|
|
3
|
-
import type { Readable } from "node:stream";
|
|
4
|
-
import { ParseError } from "../core/errors.js";
|
|
5
|
-
import type {
|
|
6
|
-
DataBatch,
|
|
7
|
-
DataStream,
|
|
8
|
-
InspectionMetadata,
|
|
9
|
-
ReaderOptions,
|
|
10
|
-
Row,
|
|
11
|
-
TabularReader,
|
|
12
|
-
} from "../core/types.js";
|
|
13
|
-
|
|
14
|
-
export interface JSONLReaderOptions extends ReaderOptions {
|
|
15
|
-
filePath?: string;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export class JSONLReader implements TabularReader {
|
|
19
|
-
private input: Readable | string;
|
|
20
|
-
private options: JSONLReaderOptions;
|
|
21
|
-
|
|
22
|
-
constructor(input: Readable | string, options: JSONLReaderOptions = {}) {
|
|
23
|
-
this.input = input;
|
|
24
|
-
this.options = { ...options };
|
|
25
|
-
if (typeof input === "string") {
|
|
26
|
-
this.options.filePath = input;
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
private getInputStream(): Readable {
|
|
31
|
-
if (typeof this.input === "string") {
|
|
32
|
-
return createReadStream(this.input);
|
|
33
|
-
}
|
|
34
|
-
return this.input;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
async *read(options?: ReaderOptions): DataStream {
|
|
38
|
-
const mergedOptions: JSONLReaderOptions = {
|
|
39
|
-
...this.options,
|
|
40
|
-
...options,
|
|
41
|
-
};
|
|
42
|
-
const batchSize = Math.max(1, mergedOptions.batchSize ?? 1000);
|
|
43
|
-
const stream = this.getInputStream();
|
|
44
|
-
|
|
45
|
-
const rl = createInterface({
|
|
46
|
-
input: stream,
|
|
47
|
-
crlfDelay: Infinity,
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
let lineNumber = 0;
|
|
51
|
-
let rowsInCurrentBatch: Row[] = [];
|
|
52
|
-
let globalOffset = 0;
|
|
53
|
-
|
|
54
|
-
for await (const line of rl) {
|
|
55
|
-
lineNumber++;
|
|
56
|
-
const trimmed = line.trim();
|
|
57
|
-
if (!trimmed) continue;
|
|
58
|
-
|
|
59
|
-
let parsed: unknown;
|
|
60
|
-
try {
|
|
61
|
-
parsed = JSON.parse(trimmed);
|
|
62
|
-
} catch (err) {
|
|
63
|
-
throw new ParseError(
|
|
64
|
-
`Invalid JSON on line ${lineNumber}: ${(err as Error).message}`,
|
|
65
|
-
{
|
|
66
|
-
file: mergedOptions.filePath,
|
|
67
|
-
line: lineNumber,
|
|
68
|
-
row: lineNumber,
|
|
69
|
-
}
|
|
70
|
-
);
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
74
|
-
throw new ParseError(
|
|
75
|
-
`Expected JSON object on line ${lineNumber}, got ${Array.isArray(parsed) ? "array" : typeof parsed}`,
|
|
76
|
-
{
|
|
77
|
-
file: mergedOptions.filePath,
|
|
78
|
-
line: lineNumber,
|
|
79
|
-
row: lineNumber,
|
|
80
|
-
}
|
|
81
|
-
);
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
rowsInCurrentBatch.push(parsed as Row);
|
|
85
|
-
|
|
86
|
-
if (rowsInCurrentBatch.length >= batchSize) {
|
|
87
|
-
yield {
|
|
88
|
-
rows: rowsInCurrentBatch,
|
|
89
|
-
offset: globalOffset,
|
|
90
|
-
};
|
|
91
|
-
globalOffset += rowsInCurrentBatch.length;
|
|
92
|
-
rowsInCurrentBatch = [];
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
if (rowsInCurrentBatch.length > 0) {
|
|
97
|
-
yield {
|
|
98
|
-
rows: rowsInCurrentBatch,
|
|
99
|
-
offset: globalOffset,
|
|
100
|
-
};
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
async inspect(options?: ReaderOptions): Promise<InspectionMetadata> {
|
|
105
|
-
let rowCount = 0;
|
|
106
|
-
let columnNames: string[] = [];
|
|
107
|
-
|
|
108
|
-
for await (const batch of this.read({ ...options, batchSize: 5000 })) {
|
|
109
|
-
if (columnNames.length === 0 && batch.rows.length > 0 && batch.rows[0]) {
|
|
110
|
-
columnNames = Object.keys(batch.rows[0]);
|
|
111
|
-
}
|
|
112
|
-
rowCount += batch.rows.length;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
return {
|
|
116
|
-
format: "JSONL",
|
|
117
|
-
rowCount,
|
|
118
|
-
columnCount: columnNames.length,
|
|
119
|
-
columns: columnNames.map((name) => ({
|
|
120
|
-
name,
|
|
121
|
-
type: "string",
|
|
122
|
-
nullPercentage: 0,
|
|
123
|
-
})),
|
|
124
|
-
};
|
|
125
|
-
}
|
|
126
|
-
}
|
package/src/readers/xlsx.ts
DELETED
|
@@ -1,182 +0,0 @@
|
|
|
1
|
-
import type { Readable } from "node:stream";
|
|
2
|
-
import readXlsxFile from "read-excel-file/node";
|
|
3
|
-
import { InvalidArgumentError, ParseError } from "../core/errors.js";
|
|
4
|
-
import type {
|
|
5
|
-
DataBatch,
|
|
6
|
-
DataStream,
|
|
7
|
-
InspectionMetadata,
|
|
8
|
-
ReaderOptions,
|
|
9
|
-
Row,
|
|
10
|
-
SheetMetadata,
|
|
11
|
-
TabularReader,
|
|
12
|
-
} from "../core/types.js";
|
|
13
|
-
|
|
14
|
-
export interface XLSXReaderOptions extends ReaderOptions {
|
|
15
|
-
sheet?: string | number;
|
|
16
|
-
filePath?: string;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export class XLSXReader implements TabularReader {
|
|
20
|
-
private input: Readable | string;
|
|
21
|
-
private options: XLSXReaderOptions;
|
|
22
|
-
|
|
23
|
-
constructor(input: Readable | string, options: XLSXReaderOptions = {}) {
|
|
24
|
-
this.input = input;
|
|
25
|
-
this.options = { ...options };
|
|
26
|
-
if (typeof input === "string") {
|
|
27
|
-
this.options.filePath = input;
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
async getParsedSheets(): Promise<Array<{ sheet: string; data: unknown[][] }>> {
|
|
32
|
-
try {
|
|
33
|
-
const result = await (readXlsxFile as unknown as (input: unknown, options?: unknown) => Promise<unknown>)(
|
|
34
|
-
this.input,
|
|
35
|
-
{ getSheets: true }
|
|
36
|
-
);
|
|
37
|
-
if (Array.isArray(result)) {
|
|
38
|
-
if (result.length > 0 && typeof result[0] === "object" && result[0] !== null && "sheet" in result[0]) {
|
|
39
|
-
return result as Array<{ sheet: string; data: unknown[][] }>;
|
|
40
|
-
}
|
|
41
|
-
return [{ sheet: "Sheet1", data: result as unknown[][] }];
|
|
42
|
-
}
|
|
43
|
-
return [];
|
|
44
|
-
} catch (err) {
|
|
45
|
-
throw new ParseError(`Failed to parse XLSX: ${(err as Error).message}`, {
|
|
46
|
-
file: this.options.filePath,
|
|
47
|
-
});
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
async *read(options?: ReaderOptions): DataStream {
|
|
52
|
-
const mergedOptions: XLSXReaderOptions = {
|
|
53
|
-
...this.options,
|
|
54
|
-
...options,
|
|
55
|
-
};
|
|
56
|
-
const batchSize = Math.max(1, mergedOptions.batchSize ?? 1000);
|
|
57
|
-
const targetSheet = mergedOptions.sheet;
|
|
58
|
-
|
|
59
|
-
const sheets = await this.getParsedSheets();
|
|
60
|
-
if (sheets.length === 0) {
|
|
61
|
-
return;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
let selectedSheet: { sheet: string; data: unknown[][] } | undefined;
|
|
65
|
-
|
|
66
|
-
if (targetSheet === undefined || targetSheet === null) {
|
|
67
|
-
selectedSheet = sheets[0];
|
|
68
|
-
} else if (typeof targetSheet === "number") {
|
|
69
|
-
selectedSheet = sheets[targetSheet - 1];
|
|
70
|
-
} else {
|
|
71
|
-
selectedSheet = sheets.find(
|
|
72
|
-
(s) => s.sheet.toLowerCase() === String(targetSheet).toLowerCase()
|
|
73
|
-
);
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
if (!selectedSheet) {
|
|
77
|
-
throw new InvalidArgumentError(
|
|
78
|
-
`Sheet "${targetSheet}" was not found in the workbook. Available sheets: ${sheets.map((s) => s.sheet).join(", ")}`
|
|
79
|
-
);
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
const grid = selectedSheet.data;
|
|
83
|
-
if (grid.length === 0) return;
|
|
84
|
-
|
|
85
|
-
let headers: string[] | null = null;
|
|
86
|
-
let rowsInCurrentBatch: Row[] = [];
|
|
87
|
-
let globalOffset = 0;
|
|
88
|
-
|
|
89
|
-
for (let r = 0; r < grid.length; r++) {
|
|
90
|
-
const rowCells = grid[r] || [];
|
|
91
|
-
if (rowCells.length === 0 || rowCells.every((c) => c === null || c === undefined || c === "")) {
|
|
92
|
-
continue;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
if (headers === null) {
|
|
96
|
-
headers = rowCells.map((c, i) => {
|
|
97
|
-
const str = c !== null && c !== undefined ? String(c).trim() : "";
|
|
98
|
-
return str.length > 0 ? str : `col_${i + 1}`;
|
|
99
|
-
});
|
|
100
|
-
continue;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
const rowObj: Row = {};
|
|
104
|
-
for (let i = 0; i < headers.length; i++) {
|
|
105
|
-
const key = headers[i] || `col_${i + 1}`;
|
|
106
|
-
const cellVal = rowCells[i];
|
|
107
|
-
rowObj[key] = cellVal !== null && cellVal !== undefined ? cellVal : "";
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
rowsInCurrentBatch.push(rowObj);
|
|
111
|
-
|
|
112
|
-
if (rowsInCurrentBatch.length >= batchSize) {
|
|
113
|
-
yield {
|
|
114
|
-
rows: rowsInCurrentBatch,
|
|
115
|
-
offset: globalOffset,
|
|
116
|
-
};
|
|
117
|
-
globalOffset += rowsInCurrentBatch.length;
|
|
118
|
-
rowsInCurrentBatch = [];
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
if (rowsInCurrentBatch.length > 0) {
|
|
123
|
-
yield {
|
|
124
|
-
rows: rowsInCurrentBatch,
|
|
125
|
-
offset: globalOffset,
|
|
126
|
-
};
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
async inspect(options?: ReaderOptions): Promise<InspectionMetadata> {
|
|
131
|
-
const sheets = await this.getParsedSheets();
|
|
132
|
-
const targetSheet = options?.sheet ?? this.options.sheet;
|
|
133
|
-
|
|
134
|
-
const sheetSummaries: SheetMetadata[] = sheets.map((s) => {
|
|
135
|
-
const nonHeaderRows = Math.max(0, s.data.length - 1);
|
|
136
|
-
const headerRow = s.data[0] || [];
|
|
137
|
-
const colHeaders = headerRow.map((c, i) =>
|
|
138
|
-
c !== null && c !== undefined ? String(c).trim() : `col_${i + 1}`
|
|
139
|
-
);
|
|
140
|
-
|
|
141
|
-
return {
|
|
142
|
-
name: s.sheet,
|
|
143
|
-
rowCount: nonHeaderRows,
|
|
144
|
-
columnCount: colHeaders.length,
|
|
145
|
-
columns: colHeaders,
|
|
146
|
-
};
|
|
147
|
-
});
|
|
148
|
-
|
|
149
|
-
let selectedSheet = sheets[0];
|
|
150
|
-
if (targetSheet !== undefined && targetSheet !== null) {
|
|
151
|
-
selectedSheet =
|
|
152
|
-
typeof targetSheet === "number"
|
|
153
|
-
? sheets[targetSheet - 1]
|
|
154
|
-
: sheets.find((s) => s.sheet.toLowerCase() === String(targetSheet).toLowerCase());
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
let columns: Array<{ name: string; type: any; nullPercentage: number }> = [];
|
|
158
|
-
let rowCount = 0;
|
|
159
|
-
|
|
160
|
-
if (selectedSheet && selectedSheet.data.length > 0) {
|
|
161
|
-
const headerRow = selectedSheet.data[0] || [];
|
|
162
|
-
const colHeaders = headerRow.map((c, i) =>
|
|
163
|
-
c !== null && c !== undefined ? String(c).trim() : `col_${i + 1}`
|
|
164
|
-
);
|
|
165
|
-
rowCount = Math.max(0, selectedSheet.data.length - 1);
|
|
166
|
-
columns = colHeaders.map((name) => ({
|
|
167
|
-
name,
|
|
168
|
-
type: "string",
|
|
169
|
-
nullPercentage: 0,
|
|
170
|
-
}));
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
return {
|
|
174
|
-
format: "XLSX",
|
|
175
|
-
rowCount,
|
|
176
|
-
columnCount: columns.length,
|
|
177
|
-
columns,
|
|
178
|
-
sheetsCount: sheets.length,
|
|
179
|
-
sheets: sheetSummaries,
|
|
180
|
-
};
|
|
181
|
-
}
|
|
182
|
-
}
|
package/src/transforms/cast.ts
DELETED
|
@@ -1,212 +0,0 @@
|
|
|
1
|
-
import { InvalidArgumentError } from "../core/errors.js";
|
|
2
|
-
import type { DataStream, Row, TransformFunction } from "../core/types.js";
|
|
3
|
-
|
|
4
|
-
export type CastOnError = "fail" | "null" | "keep" | "skip-row";
|
|
5
|
-
|
|
6
|
-
export interface CastOptions {
|
|
7
|
-
onError?: CastOnError;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Casts a single value to the target type.
|
|
12
|
-
*/
|
|
13
|
-
export function castValue(
|
|
14
|
-
value: unknown,
|
|
15
|
-
targetType: string,
|
|
16
|
-
onError: CastOnError = "null"
|
|
17
|
-
): { value: unknown; success: boolean } {
|
|
18
|
-
if (value === null || value === undefined || value === "") {
|
|
19
|
-
return { value: null, success: true };
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
const str = String(value).trim();
|
|
23
|
-
const lowerType = targetType.toLowerCase();
|
|
24
|
-
|
|
25
|
-
try {
|
|
26
|
-
switch (lowerType) {
|
|
27
|
-
case "string":
|
|
28
|
-
return { value: String(value), success: true };
|
|
29
|
-
|
|
30
|
-
case "integer":
|
|
31
|
-
case "int": {
|
|
32
|
-
if (!/^-?\d+$/.test(str)) {
|
|
33
|
-
throw new Error(`Cannot cast "${str}" to integer`);
|
|
34
|
-
}
|
|
35
|
-
const parsedInt = Number.parseInt(str, 10);
|
|
36
|
-
if (Number.isNaN(parsedInt)) {
|
|
37
|
-
throw new Error(`Cannot cast "${str}" to integer`);
|
|
38
|
-
}
|
|
39
|
-
return { value: parsedInt, success: true };
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
case "number":
|
|
43
|
-
case "float":
|
|
44
|
-
case "double": {
|
|
45
|
-
const parsedNum = Number.parseFloat(str);
|
|
46
|
-
if (Number.isNaN(parsedNum)) {
|
|
47
|
-
throw new Error(`Cannot cast "${str}" to number`);
|
|
48
|
-
}
|
|
49
|
-
return { value: parsedNum, success: true };
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
case "boolean":
|
|
53
|
-
case "bool": {
|
|
54
|
-
const lower = str.toLowerCase();
|
|
55
|
-
if (["true", "1", "yes", "t", "y"].includes(lower)) {
|
|
56
|
-
return { value: true, success: true };
|
|
57
|
-
}
|
|
58
|
-
if (["false", "0", "no", "f", "n"].includes(lower)) {
|
|
59
|
-
return { value: false, success: true };
|
|
60
|
-
}
|
|
61
|
-
throw new Error(`Cannot cast "${str}" to boolean`);
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
case "date": {
|
|
65
|
-
const d = new Date(str);
|
|
66
|
-
if (Number.isNaN(d.getTime())) {
|
|
67
|
-
throw new Error(`Cannot cast "${str}" to date`);
|
|
68
|
-
}
|
|
69
|
-
return { value: d.toISOString().split("T")[0], success: true };
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
case "datetime": {
|
|
73
|
-
const d = new Date(str);
|
|
74
|
-
if (Number.isNaN(d.getTime())) {
|
|
75
|
-
throw new Error(`Cannot cast "${str}" to datetime`);
|
|
76
|
-
}
|
|
77
|
-
return { value: d.toISOString(), success: true };
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
case "json": {
|
|
81
|
-
const parsed = JSON.parse(str);
|
|
82
|
-
return { value: parsed, success: true };
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
default:
|
|
86
|
-
throw new Error(`Unknown target type "${targetType}"`);
|
|
87
|
-
}
|
|
88
|
-
} catch (err) {
|
|
89
|
-
if (onError === "fail") {
|
|
90
|
-
throw new InvalidArgumentError(
|
|
91
|
-
`Failed to cast value "${str}" to type "${targetType}": ${(err as Error).message}`
|
|
92
|
-
);
|
|
93
|
-
}
|
|
94
|
-
if (onError === "keep") {
|
|
95
|
-
return { value, success: false };
|
|
96
|
-
}
|
|
97
|
-
// "null" or "skip-row"
|
|
98
|
-
return { value: null, success: false };
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
/**
|
|
103
|
-
* Creates a transform that casts specified columns to target types.
|
|
104
|
-
*/
|
|
105
|
-
export function castColumns(
|
|
106
|
-
typeSpecs: Record<string, string>,
|
|
107
|
-
options: CastOptions = {}
|
|
108
|
-
): TransformFunction {
|
|
109
|
-
const onError = options.onError || "null";
|
|
110
|
-
|
|
111
|
-
const compiled = Object.entries(typeSpecs).map(([col, targetType]) => {
|
|
112
|
-
const lower = targetType.toLowerCase();
|
|
113
|
-
|
|
114
|
-
// Fast-path specialized caster functions
|
|
115
|
-
if (lower === "number" || lower === "float" || lower === "double") {
|
|
116
|
-
return {
|
|
117
|
-
col,
|
|
118
|
-
cast: (val: unknown) => {
|
|
119
|
-
if (val === null || val === undefined || val === "") return { value: null, success: true };
|
|
120
|
-
const num = typeof val === "number" ? val : Number(val);
|
|
121
|
-
if (!Number.isNaN(num)) return { value: num, success: true };
|
|
122
|
-
if (onError === "fail") throw new InvalidArgumentError(`Cannot cast "${String(val)}" to number`);
|
|
123
|
-
if (onError === "keep") return { value: val, success: false };
|
|
124
|
-
return { value: null, success: false };
|
|
125
|
-
},
|
|
126
|
-
};
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
if (lower === "integer" || lower === "int") {
|
|
130
|
-
return {
|
|
131
|
-
col,
|
|
132
|
-
cast: (val: unknown) => {
|
|
133
|
-
if (val === null || val === undefined || val === "") return { value: null, success: true };
|
|
134
|
-
const str = String(val).trim();
|
|
135
|
-
if (!/^-?\d+$/.test(str)) {
|
|
136
|
-
if (onError === "fail") throw new InvalidArgumentError(`Cannot cast "${str}" to integer`);
|
|
137
|
-
if (onError === "keep") return { value: val, success: false };
|
|
138
|
-
return { value: null, success: false };
|
|
139
|
-
}
|
|
140
|
-
const num = Number.parseInt(str, 10);
|
|
141
|
-
return { value: num, success: true };
|
|
142
|
-
},
|
|
143
|
-
};
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
if (lower === "string") {
|
|
147
|
-
return {
|
|
148
|
-
col,
|
|
149
|
-
cast: (val: unknown) => ({ value: val !== null && val !== undefined ? String(val) : "", success: true }),
|
|
150
|
-
};
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
return {
|
|
154
|
-
col,
|
|
155
|
-
cast: (val: unknown) => castValue(val, targetType, onError),
|
|
156
|
-
};
|
|
157
|
-
});
|
|
158
|
-
|
|
159
|
-
const compiledLen = compiled.length;
|
|
160
|
-
|
|
161
|
-
return function (stream: DataStream): DataStream {
|
|
162
|
-
return (async function* () {
|
|
163
|
-
for await (const batch of stream) {
|
|
164
|
-
const rows = batch.rows;
|
|
165
|
-
const rowCount = rows.length;
|
|
166
|
-
const transformedRows: Row[] = [];
|
|
167
|
-
|
|
168
|
-
for (let i = 0; i < rowCount; i++) {
|
|
169
|
-
const row = rows[i]!;
|
|
170
|
-
let skipRow = false;
|
|
171
|
-
const newRow: Row = { ...row };
|
|
172
|
-
|
|
173
|
-
for (let j = 0; j < compiledLen; j++) {
|
|
174
|
-
const entry = compiled[j]!;
|
|
175
|
-
const col = entry.col;
|
|
176
|
-
if (col in newRow) {
|
|
177
|
-
const res = entry.cast(newRow[col]);
|
|
178
|
-
if (!res.success && onError === "skip-row") {
|
|
179
|
-
skipRow = true;
|
|
180
|
-
break;
|
|
181
|
-
}
|
|
182
|
-
newRow[col] = res.value;
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
if (!skipRow) {
|
|
187
|
-
transformedRows.push(newRow);
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
yield {
|
|
192
|
-
rows: transformedRows,
|
|
193
|
-
offset: batch.offset,
|
|
194
|
-
};
|
|
195
|
-
}
|
|
196
|
-
})();
|
|
197
|
-
};
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
/**
|
|
201
|
-
* Parses command line cast specs like ["age:integer", "revenue:number"].
|
|
202
|
-
*/
|
|
203
|
-
export function parseCastSpecs(specs: string[]): Record<string, string> {
|
|
204
|
-
const mapping: Record<string, string> = {};
|
|
205
|
-
for (const spec of specs) {
|
|
206
|
-
const parts = spec.split(":");
|
|
207
|
-
if (parts.length === 2 && parts[0] && parts[1]) {
|
|
208
|
-
mapping[parts[0].trim()] = parts[1].trim();
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
return mapping;
|
|
212
|
-
}
|