rowpipe 1.0.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 +401 -0
- package/benchmarks/memory-bench.ts +109 -0
- package/dist/analytics/reduce.d.ts +33 -0
- package/dist/analytics/reduce.d.ts.map +1 -0
- package/dist/analytics/reduce.js +318 -0
- package/dist/analytics/reduce.js.map +1 -0
- package/dist/analytics/schema-inference.d.ts +31 -0
- package/dist/analytics/schema-inference.d.ts.map +1 -0
- package/dist/analytics/schema-inference.js +166 -0
- package/dist/analytics/schema-inference.js.map +1 -0
- package/dist/analytics/semantic-types.d.ts +6 -0
- package/dist/analytics/semantic-types.d.ts.map +1 -0
- package/dist/analytics/semantic-types.js +40 -0
- package/dist/analytics/semantic-types.js.map +1 -0
- package/dist/analytics/stats.d.ts +102 -0
- package/dist/analytics/stats.d.ts.map +1 -0
- package/dist/analytics/stats.js +307 -0
- package/dist/analytics/stats.js.map +1 -0
- package/dist/analytics/validator.d.ts +32 -0
- package/dist/analytics/validator.d.ts.map +1 -0
- package/dist/analytics/validator.js +96 -0
- package/dist/analytics/validator.js.map +1 -0
- package/dist/cli/commands/cast.d.ts +13 -0
- package/dist/cli/commands/cast.d.ts.map +1 -0
- package/dist/cli/commands/cast.js +39 -0
- package/dist/cli/commands/cast.js.map +1 -0
- package/dist/cli/commands/convert.d.ts +15 -0
- package/dist/cli/commands/convert.d.ts.map +1 -0
- package/dist/cli/commands/convert.js +89 -0
- package/dist/cli/commands/convert.js.map +1 -0
- package/dist/cli/commands/filter.d.ts +11 -0
- package/dist/cli/commands/filter.d.ts.map +1 -0
- package/dist/cli/commands/filter.js +38 -0
- package/dist/cli/commands/filter.js.map +1 -0
- package/dist/cli/commands/inspect.d.ts +10 -0
- package/dist/cli/commands/inspect.d.ts.map +1 -0
- package/dist/cli/commands/inspect.js +134 -0
- package/dist/cli/commands/inspect.js.map +1 -0
- package/dist/cli/commands/map.d.ts +11 -0
- package/dist/cli/commands/map.d.ts.map +1 -0
- package/dist/cli/commands/map.js +43 -0
- package/dist/cli/commands/map.js.map +1 -0
- package/dist/cli/commands/reduce.d.ts +12 -0
- package/dist/cli/commands/reduce.d.ts.map +1 -0
- package/dist/cli/commands/reduce.js +49 -0
- package/dist/cli/commands/reduce.js.map +1 -0
- package/dist/cli/commands/rename.d.ts +11 -0
- package/dist/cli/commands/rename.d.ts.map +1 -0
- package/dist/cli/commands/rename.js +39 -0
- package/dist/cli/commands/rename.js.map +1 -0
- package/dist/cli/commands/sample.d.ts +13 -0
- package/dist/cli/commands/sample.d.ts.map +1 -0
- package/dist/cli/commands/sample.js +40 -0
- package/dist/cli/commands/sample.js.map +1 -0
- package/dist/cli/commands/schema.d.ts +13 -0
- package/dist/cli/commands/schema.d.ts.map +1 -0
- package/dist/cli/commands/schema.js +57 -0
- package/dist/cli/commands/schema.js.map +1 -0
- package/dist/cli/commands/select.d.ts +11 -0
- package/dist/cli/commands/select.d.ts.map +1 -0
- package/dist/cli/commands/select.js +39 -0
- package/dist/cli/commands/select.js.map +1 -0
- package/dist/cli/commands/stats.d.ts +15 -0
- package/dist/cli/commands/stats.d.ts.map +1 -0
- package/dist/cli/commands/stats.js +75 -0
- package/dist/cli/commands/stats.js.map +1 -0
- package/dist/cli/commands/validate.d.ts +12 -0
- package/dist/cli/commands/validate.d.ts.map +1 -0
- package/dist/cli/commands/validate.js +86 -0
- package/dist/cli/commands/validate.js.map +1 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +262 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/core/batch.d.ts +15 -0
- package/dist/core/batch.d.ts.map +1 -0
- package/dist/core/batch.js +67 -0
- package/dist/core/batch.js.map +1 -0
- package/dist/core/errors.d.ts +35 -0
- package/dist/core/errors.d.ts.map +1 -0
- package/dist/core/errors.js +55 -0
- package/dist/core/errors.js.map +1 -0
- package/dist/core/pipeline.d.ts +40 -0
- package/dist/core/pipeline.d.ts.map +1 -0
- package/dist/core/pipeline.js +120 -0
- package/dist/core/pipeline.js.map +1 -0
- package/dist/core/types.d.ts +73 -0
- package/dist/core/types.d.ts.map +1 -0
- package/dist/core/types.js +2 -0
- package/dist/core/types.js.map +1 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +28 -0
- package/dist/index.js.map +1 -0
- package/dist/readers/csv.d.ts +24 -0
- package/dist/readers/csv.d.ts.map +1 -0
- package/dist/readers/csv.js +251 -0
- package/dist/readers/csv.js.map +1 -0
- package/dist/readers/index.d.ts +17 -0
- package/dist/readers/index.d.ts.map +1 -0
- package/dist/readers/index.js +74 -0
- package/dist/readers/index.js.map +1 -0
- package/dist/readers/json.d.ts +20 -0
- package/dist/readers/json.d.ts.map +1 -0
- package/dist/readers/json.js +219 -0
- package/dist/readers/json.js.map +1 -0
- package/dist/readers/jsonl.d.ts +14 -0
- package/dist/readers/jsonl.d.ts.map +1 -0
- package/dist/readers/jsonl.js +95 -0
- package/dist/readers/jsonl.js.map +1 -0
- package/dist/readers/xlsx.d.ts +18 -0
- package/dist/readers/xlsx.d.ts.map +1 -0
- package/dist/readers/xlsx.js +138 -0
- package/dist/readers/xlsx.js.map +1 -0
- package/dist/transforms/cast.d.ts +21 -0
- package/dist/transforms/cast.d.ts.map +1 -0
- package/dist/transforms/cast.js +182 -0
- package/dist/transforms/cast.js.map +1 -0
- package/dist/transforms/expression.d.ts +67 -0
- package/dist/transforms/expression.d.ts.map +1 -0
- package/dist/transforms/expression.js +943 -0
- package/dist/transforms/expression.js.map +1 -0
- package/dist/transforms/filter.d.ts +6 -0
- package/dist/transforms/filter.d.ts.map +1 -0
- package/dist/transforms/filter.js +31 -0
- package/dist/transforms/filter.js.map +1 -0
- package/dist/transforms/map.d.ts +12 -0
- package/dist/transforms/map.d.ts.map +1 -0
- package/dist/transforms/map.js +76 -0
- package/dist/transforms/map.js.map +1 -0
- package/dist/transforms/rename.d.ts +10 -0
- package/dist/transforms/rename.d.ts.map +1 -0
- package/dist/transforms/rename.js +39 -0
- package/dist/transforms/rename.js.map +1 -0
- package/dist/transforms/sample.d.ts +10 -0
- package/dist/transforms/sample.d.ts.map +1 -0
- package/dist/transforms/sample.js +47 -0
- package/dist/transforms/sample.js.map +1 -0
- package/dist/transforms/select.d.ts +6 -0
- package/dist/transforms/select.d.ts.map +1 -0
- package/dist/transforms/select.js +63 -0
- package/dist/transforms/select.js.map +1 -0
- package/dist/utils/compression.d.ts +14 -0
- package/dist/utils/compression.d.ts.map +1 -0
- package/dist/utils/compression.js +38 -0
- package/dist/utils/compression.js.map +1 -0
- package/dist/utils/formatting.d.ts +6 -0
- package/dist/utils/formatting.d.ts.map +1 -0
- package/dist/utils/formatting.js +56 -0
- package/dist/utils/formatting.js.map +1 -0
- package/dist/utils/progress.d.ts +13 -0
- package/dist/utils/progress.d.ts.map +1 -0
- package/dist/utils/progress.js +27 -0
- package/dist/utils/progress.js.map +1 -0
- package/dist/writers/csv.d.ts +18 -0
- package/dist/writers/csv.d.ts.map +1 -0
- package/dist/writers/csv.js +84 -0
- package/dist/writers/csv.js.map +1 -0
- package/dist/writers/index.d.ts +17 -0
- package/dist/writers/index.d.ts.map +1 -0
- package/dist/writers/index.js +73 -0
- package/dist/writers/index.js.map +1 -0
- package/dist/writers/json.d.ts +12 -0
- package/dist/writers/json.d.ts.map +1 -0
- package/dist/writers/json.js +42 -0
- package/dist/writers/json.js.map +1 -0
- package/dist/writers/jsonl.d.ts +12 -0
- package/dist/writers/jsonl.d.ts.map +1 -0
- package/dist/writers/jsonl.js +41 -0
- package/dist/writers/jsonl.js.map +1 -0
- package/dist/writers/xlsx.d.ts +13 -0
- package/dist/writers/xlsx.d.ts.map +1 -0
- package/dist/writers/xlsx.js +65 -0
- package/dist/writers/xlsx.js.map +1 -0
- package/package.json +44 -0
- package/src/analytics/reduce.ts +388 -0
- package/src/analytics/schema-inference.ts +217 -0
- package/src/analytics/semantic-types.ts +36 -0
- package/src/analytics/stats.ts +383 -0
- package/src/analytics/validator.ts +134 -0
- package/src/cli/commands/cast.ts +60 -0
- package/src/cli/commands/convert.ts +128 -0
- package/src/cli/commands/filter.ts +58 -0
- package/src/cli/commands/inspect.ts +193 -0
- package/src/cli/commands/map.ts +66 -0
- package/src/cli/commands/reduce.ts +74 -0
- package/src/cli/commands/rename.ts +59 -0
- package/src/cli/commands/sample.ts +61 -0
- package/src/cli/commands/schema.ts +86 -0
- package/src/cli/commands/select.ts +59 -0
- package/src/cli/commands/stats.ts +100 -0
- package/src/cli/commands/validate.ts +123 -0
- package/src/cli/index.ts +285 -0
- package/src/core/batch.ts +81 -0
- package/src/core/errors.ts +81 -0
- package/src/core/pipeline.ts +150 -0
- package/src/core/types.ts +110 -0
- package/src/index.ts +52 -0
- package/src/readers/csv.ts +302 -0
- package/src/readers/index.ts +88 -0
- package/src/readers/json.ts +256 -0
- package/src/readers/jsonl.ts +126 -0
- package/src/readers/xlsx.ts +182 -0
- package/src/transforms/cast.ts +212 -0
- package/src/transforms/expression.ts +1114 -0
- package/src/transforms/filter.ts +38 -0
- package/src/transforms/map.ts +99 -0
- package/src/transforms/rename.ts +47 -0
- package/src/transforms/sample.ts +57 -0
- package/src/transforms/select.ts +64 -0
- package/src/utils/compression.ts +43 -0
- package/src/utils/formatting.ts +75 -0
- package/src/utils/progress.ts +37 -0
- package/src/writers/csv.ts +106 -0
- package/src/writers/index.ts +87 -0
- package/src/writers/json.ts +52 -0
- package/src/writers/jsonl.ts +49 -0
- package/src/writers/xlsx.ts +87 -0
- package/tests/analytics.test.ts +119 -0
- package/tests/cli-integration.test.ts +187 -0
- package/tests/map-reduce.test.ts +223 -0
- package/tests/readers-writers.test.ts +189 -0
- package/tests/transforms.test.ts +199 -0
- package/tsconfig.json +21 -0
- package/vitest.config.ts +9 -0
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
Aggregator,
|
|
3
|
+
DataBatch,
|
|
4
|
+
DataStream,
|
|
5
|
+
ProgressCallback,
|
|
6
|
+
ReaderOptions,
|
|
7
|
+
Row,
|
|
8
|
+
TabularReader,
|
|
9
|
+
TabularWriter,
|
|
10
|
+
TransformFunction,
|
|
11
|
+
WriterOptions,
|
|
12
|
+
} from "./types.js";
|
|
13
|
+
import { batchesToRows } from "./batch.js";
|
|
14
|
+
|
|
15
|
+
export class Pipeline {
|
|
16
|
+
private stream: DataStream;
|
|
17
|
+
private progressCallbacks: ProgressCallback[] = [];
|
|
18
|
+
|
|
19
|
+
constructor(source: DataStream | TabularReader, options?: ReaderOptions) {
|
|
20
|
+
if ("read" in source && typeof source.read === "function") {
|
|
21
|
+
this.stream = source.read(options);
|
|
22
|
+
} else {
|
|
23
|
+
this.stream = source as DataStream;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Chains a transform function to the pipeline.
|
|
29
|
+
*/
|
|
30
|
+
pipe(transform: TransformFunction): this {
|
|
31
|
+
this.stream = transform(this.stream);
|
|
32
|
+
return this;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Adds a progress listener.
|
|
37
|
+
*/
|
|
38
|
+
onProgress(callback: ProgressCallback): this {
|
|
39
|
+
this.progressCallbacks.push(callback);
|
|
40
|
+
return this;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Wraps the current stream with a progress tracking generator if callbacks are registered.
|
|
45
|
+
*/
|
|
46
|
+
private getTrackedStream(): DataStream {
|
|
47
|
+
if (this.progressCallbacks.length === 0) {
|
|
48
|
+
return this.stream;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const callbacks = this.progressCallbacks;
|
|
52
|
+
const stream = this.stream;
|
|
53
|
+
|
|
54
|
+
return (async function* () {
|
|
55
|
+
let rowsProcessed = 0;
|
|
56
|
+
const startTime = Date.now();
|
|
57
|
+
let lastReport = startTime;
|
|
58
|
+
|
|
59
|
+
for await (const batch of stream) {
|
|
60
|
+
rowsProcessed += batch.rows.length;
|
|
61
|
+
const now = Date.now();
|
|
62
|
+
|
|
63
|
+
// Throttle progress notifications to ~100ms intervals
|
|
64
|
+
if (now - lastReport >= 100) {
|
|
65
|
+
const elapsedSec = (now - startTime) / 1000 || 0.001;
|
|
66
|
+
const info = {
|
|
67
|
+
rowsProcessed,
|
|
68
|
+
elapsedMs: now - startTime,
|
|
69
|
+
rowsPerSecond: Math.round(rowsProcessed / elapsedSec),
|
|
70
|
+
};
|
|
71
|
+
for (const cb of callbacks) {
|
|
72
|
+
cb(info);
|
|
73
|
+
}
|
|
74
|
+
lastReport = now;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
yield batch;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Final progress callback
|
|
81
|
+
const finalNow = Date.now();
|
|
82
|
+
const finalElapsedSec = (finalNow - startTime) / 1000 || 0.001;
|
|
83
|
+
const finalInfo = {
|
|
84
|
+
rowsProcessed,
|
|
85
|
+
elapsedMs: finalNow - startTime,
|
|
86
|
+
rowsPerSecond: Math.round(rowsProcessed / finalElapsedSec),
|
|
87
|
+
};
|
|
88
|
+
for (const cb of callbacks) {
|
|
89
|
+
cb(finalInfo);
|
|
90
|
+
}
|
|
91
|
+
})();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Pipes the stream into a TabularWriter.
|
|
96
|
+
*/
|
|
97
|
+
async to(writer: TabularWriter, options?: WriterOptions): Promise<void> {
|
|
98
|
+
const tracked = this.getTrackedStream();
|
|
99
|
+
await writer.write(tracked, options);
|
|
100
|
+
if (writer.close) {
|
|
101
|
+
await writer.close();
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Consumes the stream using an Aggregator.
|
|
107
|
+
*/
|
|
108
|
+
async reduce<T>(aggregator: Aggregator<T>): Promise<T> {
|
|
109
|
+
const tracked = this.getTrackedStream();
|
|
110
|
+
for await (const row of batchesToRows(tracked)) {
|
|
111
|
+
aggregator.add(row);
|
|
112
|
+
}
|
|
113
|
+
return aggregator.result();
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Returns an AsyncIterable of DataBatches.
|
|
118
|
+
*/
|
|
119
|
+
batches(): DataStream {
|
|
120
|
+
return this.getTrackedStream();
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Returns an AsyncIterable of individual rows.
|
|
125
|
+
*/
|
|
126
|
+
rows(): AsyncIterable<Row> {
|
|
127
|
+
return batchesToRows(this.getTrackedStream());
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Collects all rows up to a maximum limit (default 10,000 to prevent unbounded memory).
|
|
132
|
+
*/
|
|
133
|
+
async toArray(maxRows = 10000): Promise<Row[]> {
|
|
134
|
+
const results: Row[] = [];
|
|
135
|
+
for await (const row of this.rows()) {
|
|
136
|
+
results.push(row);
|
|
137
|
+
if (results.length >= maxRows) {
|
|
138
|
+
break;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return results;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function createPipeline(
|
|
146
|
+
source: DataStream | TabularReader,
|
|
147
|
+
options?: ReaderOptions
|
|
148
|
+
): Pipeline {
|
|
149
|
+
return new Pipeline(source, options);
|
|
150
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import type { Readable, Writable } from "node:stream";
|
|
2
|
+
|
|
3
|
+
export type Row = Record<string, unknown>;
|
|
4
|
+
|
|
5
|
+
export interface DataBatch {
|
|
6
|
+
rows: Row[];
|
|
7
|
+
offset: number;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export type DataStream = AsyncIterable<DataBatch>;
|
|
11
|
+
|
|
12
|
+
export type ColumnType =
|
|
13
|
+
| "string"
|
|
14
|
+
| "integer"
|
|
15
|
+
| "number"
|
|
16
|
+
| "boolean"
|
|
17
|
+
| "date"
|
|
18
|
+
| "datetime"
|
|
19
|
+
| "null"
|
|
20
|
+
| "mixed";
|
|
21
|
+
|
|
22
|
+
export type SemanticType =
|
|
23
|
+
| "email"
|
|
24
|
+
| "url"
|
|
25
|
+
| "uuid"
|
|
26
|
+
| "ipv4"
|
|
27
|
+
| "ipv6"
|
|
28
|
+
| "country-code"
|
|
29
|
+
| "currency"
|
|
30
|
+
| "phone";
|
|
31
|
+
|
|
32
|
+
export interface ReaderOptions {
|
|
33
|
+
batchSize?: number;
|
|
34
|
+
header?: boolean;
|
|
35
|
+
delimiter?: string;
|
|
36
|
+
path?: string;
|
|
37
|
+
sheet?: string | number;
|
|
38
|
+
maxRows?: number;
|
|
39
|
+
filePath?: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface WriterOptions {
|
|
43
|
+
delimiter?: string;
|
|
44
|
+
header?: boolean;
|
|
45
|
+
sheet?: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface SheetMetadata {
|
|
49
|
+
name: string;
|
|
50
|
+
rowCount: number;
|
|
51
|
+
columnCount?: number;
|
|
52
|
+
columns?: string[];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface InspectionMetadata {
|
|
56
|
+
format: string;
|
|
57
|
+
sizeBytes?: number;
|
|
58
|
+
rowCount?: number;
|
|
59
|
+
columnCount?: number;
|
|
60
|
+
columns?: Array<{
|
|
61
|
+
name: string;
|
|
62
|
+
type: ColumnType;
|
|
63
|
+
nullPercentage: number;
|
|
64
|
+
approxUniquePercentage?: number;
|
|
65
|
+
}>;
|
|
66
|
+
sheetsCount?: number;
|
|
67
|
+
sheets?: SheetMetadata[];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface TabularReader {
|
|
71
|
+
read(options?: ReaderOptions): DataStream;
|
|
72
|
+
inspect?(options?: ReaderOptions): Promise<InspectionMetadata>;
|
|
73
|
+
close?(): Promise<void>;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export interface TabularWriter {
|
|
77
|
+
write(stream: DataStream, options?: WriterOptions): Promise<void>;
|
|
78
|
+
close?(): Promise<void>;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export interface FormatAdapter {
|
|
82
|
+
name: string;
|
|
83
|
+
extensions: string[];
|
|
84
|
+
createReader(
|
|
85
|
+
input: Readable | string,
|
|
86
|
+
options?: ReaderOptions
|
|
87
|
+
): TabularReader;
|
|
88
|
+
createWriter(
|
|
89
|
+
output: Writable | string,
|
|
90
|
+
options?: WriterOptions
|
|
91
|
+
): TabularWriter;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export type TransformFunction = (stream: DataStream) => DataStream;
|
|
95
|
+
|
|
96
|
+
export interface Aggregator<T> {
|
|
97
|
+
add(row: Row): void;
|
|
98
|
+
merge?(other: Aggregator<T>): void;
|
|
99
|
+
result(): T;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export interface ProgressCallbackInfo {
|
|
103
|
+
rowsProcessed: number;
|
|
104
|
+
bytesRead?: number;
|
|
105
|
+
elapsedMs: number;
|
|
106
|
+
rowsPerSecond: number;
|
|
107
|
+
megabytesPerSecond?: number;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export type ProgressCallback = (info: ProgressCallbackInfo) => void;
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// Core
|
|
2
|
+
export * from "./core/types.js";
|
|
3
|
+
export * from "./core/errors.js";
|
|
4
|
+
export * from "./core/batch.js";
|
|
5
|
+
export * from "./core/pipeline.js";
|
|
6
|
+
|
|
7
|
+
// Readers
|
|
8
|
+
export {
|
|
9
|
+
CSVReader,
|
|
10
|
+
type CSVReaderOptions,
|
|
11
|
+
JSONReader,
|
|
12
|
+
type JSONReaderOptions,
|
|
13
|
+
JSONLReader,
|
|
14
|
+
type JSONLReaderOptions,
|
|
15
|
+
XLSXReader,
|
|
16
|
+
type XLSXReaderOptions,
|
|
17
|
+
createReader,
|
|
18
|
+
inferFormatFromPath as inferReaderFormatFromPath,
|
|
19
|
+
} from "./readers/index.js";
|
|
20
|
+
|
|
21
|
+
// Writers
|
|
22
|
+
export {
|
|
23
|
+
CSVWriter,
|
|
24
|
+
type CSVWriterOptions,
|
|
25
|
+
JSONWriter,
|
|
26
|
+
JSONLWriter,
|
|
27
|
+
XLSXWriter,
|
|
28
|
+
type XLSXWriterOptions,
|
|
29
|
+
createWriter,
|
|
30
|
+
inferFormatFromPath as inferWriterFormatFromPath,
|
|
31
|
+
} from "./writers/index.js";
|
|
32
|
+
|
|
33
|
+
// Transforms
|
|
34
|
+
export * from "./transforms/select.js";
|
|
35
|
+
export * from "./transforms/rename.js";
|
|
36
|
+
export * from "./transforms/cast.js";
|
|
37
|
+
export * from "./transforms/sample.js";
|
|
38
|
+
export * from "./transforms/filter.js";
|
|
39
|
+
export * from "./transforms/expression.js";
|
|
40
|
+
export * from "./transforms/map.js";
|
|
41
|
+
|
|
42
|
+
// Analytics
|
|
43
|
+
export * from "./analytics/stats.js";
|
|
44
|
+
export * from "./analytics/reduce.js";
|
|
45
|
+
export * from "./analytics/schema-inference.js";
|
|
46
|
+
export * from "./analytics/semantic-types.js";
|
|
47
|
+
export * from "./analytics/validator.js";
|
|
48
|
+
|
|
49
|
+
// Utilities
|
|
50
|
+
export * from "./utils/compression.js";
|
|
51
|
+
export * from "./utils/formatting.js";
|
|
52
|
+
export * from "./utils/progress.js";
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
import { createReadStream } from "node:fs";
|
|
2
|
+
import type { Readable } from "node:stream";
|
|
3
|
+
import { ParseError } from "../core/errors.js";
|
|
4
|
+
import type {
|
|
5
|
+
DataBatch,
|
|
6
|
+
DataStream,
|
|
7
|
+
InspectionMetadata,
|
|
8
|
+
ReaderOptions,
|
|
9
|
+
Row,
|
|
10
|
+
TabularReader,
|
|
11
|
+
} from "../core/types.js";
|
|
12
|
+
|
|
13
|
+
export interface CSVReaderOptions extends ReaderOptions {
|
|
14
|
+
delimiter?: string;
|
|
15
|
+
header?: boolean;
|
|
16
|
+
filePath?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Streaming CSV Reader supporting RFC 4180 quotes, multiline cells,
|
|
21
|
+
* custom & auto-detected delimiters, BOM removal, and exact error reporting.
|
|
22
|
+
*/
|
|
23
|
+
export class CSVReader implements TabularReader {
|
|
24
|
+
private input: Readable | string;
|
|
25
|
+
private options: CSVReaderOptions;
|
|
26
|
+
|
|
27
|
+
constructor(input: Readable | string, options: CSVReaderOptions = {}) {
|
|
28
|
+
this.input = input;
|
|
29
|
+
this.options = { ...options };
|
|
30
|
+
if (typeof input === "string") {
|
|
31
|
+
this.options.filePath = input;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
private getInputStream(): Readable {
|
|
36
|
+
if (typeof this.input === "string") {
|
|
37
|
+
return createReadStream(this.input);
|
|
38
|
+
}
|
|
39
|
+
return this.input;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Auto-detects delimiter by scanning candidate delimiters in a sample string.
|
|
44
|
+
*/
|
|
45
|
+
public static detectDelimiter(sample: string): string {
|
|
46
|
+
const candidates = [",", "\t", ";", "|"];
|
|
47
|
+
const firstLine = sample.split(/\r?\n/)[0] || "";
|
|
48
|
+
|
|
49
|
+
let bestDelimiter = ",";
|
|
50
|
+
let maxCount = -1;
|
|
51
|
+
|
|
52
|
+
for (const cand of candidates) {
|
|
53
|
+
let count = 0;
|
|
54
|
+
let inQuotes = false;
|
|
55
|
+
for (let i = 0; i < firstLine.length; i++) {
|
|
56
|
+
const ch = firstLine[i];
|
|
57
|
+
if (ch === '"') {
|
|
58
|
+
inQuotes = !inQuotes;
|
|
59
|
+
} else if (ch === cand && !inQuotes) {
|
|
60
|
+
count++;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
if (count > maxCount) {
|
|
64
|
+
maxCount = count;
|
|
65
|
+
bestDelimiter = cand;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return maxCount > 0 ? bestDelimiter : ",";
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async *read(options?: ReaderOptions): DataStream {
|
|
73
|
+
const mergedOptions: CSVReaderOptions = {
|
|
74
|
+
...this.options,
|
|
75
|
+
...options,
|
|
76
|
+
};
|
|
77
|
+
const batchSize = Math.max(1, mergedOptions.batchSize ?? 1000);
|
|
78
|
+
const hasHeader = mergedOptions.header !== false;
|
|
79
|
+
let delimiter = mergedOptions.delimiter;
|
|
80
|
+
|
|
81
|
+
const stream = this.getInputStream();
|
|
82
|
+
|
|
83
|
+
let buffer = "";
|
|
84
|
+
let headers: string[] | null = null;
|
|
85
|
+
let isFirstChunk = true;
|
|
86
|
+
let rowsInCurrentBatch: Row[] = [];
|
|
87
|
+
let globalRowOffset = 0;
|
|
88
|
+
let rowNumber = 0;
|
|
89
|
+
let byteOffset = 0;
|
|
90
|
+
let currentField = "";
|
|
91
|
+
let currentRowFields: string[] = [];
|
|
92
|
+
let inQuotes = false;
|
|
93
|
+
let cursor = 0;
|
|
94
|
+
|
|
95
|
+
for await (const chunk of stream) {
|
|
96
|
+
const chunkStr: string =
|
|
97
|
+
typeof chunk === "string" ? chunk : (chunk as Buffer).toString("utf8");
|
|
98
|
+
|
|
99
|
+
if (isFirstChunk) {
|
|
100
|
+
isFirstChunk = false;
|
|
101
|
+
// Remove UTF-8 BOM if present
|
|
102
|
+
if (chunkStr.charCodeAt(0) === 0xfeff) {
|
|
103
|
+
buffer = chunkStr.slice(1);
|
|
104
|
+
} else {
|
|
105
|
+
buffer = chunkStr;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Auto-detect delimiter if not specified
|
|
109
|
+
if (!delimiter) {
|
|
110
|
+
delimiter = CSVReader.detectDelimiter(buffer);
|
|
111
|
+
}
|
|
112
|
+
} else {
|
|
113
|
+
buffer += chunkStr;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const activeDelimiter = delimiter ?? ",";
|
|
117
|
+
const delimLen = activeDelimiter.length;
|
|
118
|
+
|
|
119
|
+
while (cursor < buffer.length) {
|
|
120
|
+
const char = buffer[cursor];
|
|
121
|
+
|
|
122
|
+
if (inQuotes) {
|
|
123
|
+
if (char === '"') {
|
|
124
|
+
if (cursor + 1 < buffer.length && buffer[cursor + 1] === '"') {
|
|
125
|
+
currentField += '"';
|
|
126
|
+
cursor += 2;
|
|
127
|
+
byteOffset += 2;
|
|
128
|
+
continue;
|
|
129
|
+
} else {
|
|
130
|
+
inQuotes = false;
|
|
131
|
+
cursor++;
|
|
132
|
+
byteOffset++;
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
} else {
|
|
136
|
+
currentField += char;
|
|
137
|
+
cursor++;
|
|
138
|
+
byteOffset++;
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
} else {
|
|
142
|
+
if (char === '"') {
|
|
143
|
+
if (currentField.length === 0) {
|
|
144
|
+
inQuotes = true;
|
|
145
|
+
cursor++;
|
|
146
|
+
byteOffset++;
|
|
147
|
+
continue;
|
|
148
|
+
} else {
|
|
149
|
+
throw new ParseError(
|
|
150
|
+
`Unexpected quote inside unquoted field at byte offset ${byteOffset}`,
|
|
151
|
+
{
|
|
152
|
+
file: mergedOptions.filePath,
|
|
153
|
+
row: rowNumber + 1,
|
|
154
|
+
column: currentRowFields.length + 1,
|
|
155
|
+
byteOffset,
|
|
156
|
+
}
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Check for delimiter
|
|
162
|
+
if (buffer.startsWith(activeDelimiter, cursor)) {
|
|
163
|
+
currentRowFields.push(currentField);
|
|
164
|
+
currentField = "";
|
|
165
|
+
cursor += delimLen;
|
|
166
|
+
byteOffset += delimLen;
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Check for CRLF / LF newline
|
|
171
|
+
if (char === "\r" || char === "\n") {
|
|
172
|
+
currentRowFields.push(currentField);
|
|
173
|
+
currentField = "";
|
|
174
|
+
|
|
175
|
+
if (char === "\r" && cursor + 1 < buffer.length && buffer[cursor + 1] === "\n") {
|
|
176
|
+
cursor += 2;
|
|
177
|
+
byteOffset += 2;
|
|
178
|
+
} else {
|
|
179
|
+
cursor++;
|
|
180
|
+
byteOffset++;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Skip entirely empty row if it's trailing whitespace
|
|
184
|
+
if (
|
|
185
|
+
currentRowFields.length === 1 &&
|
|
186
|
+
currentRowFields[0]?.trim() === "" &&
|
|
187
|
+
cursor >= buffer.length
|
|
188
|
+
) {
|
|
189
|
+
currentRowFields = [];
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (hasHeader && headers === null) {
|
|
194
|
+
headers = currentRowFields.map((h, i) =>
|
|
195
|
+
h.trim().length > 0 ? h.trim() : `col_${i + 1}`
|
|
196
|
+
);
|
|
197
|
+
currentRowFields = [];
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (headers === null) {
|
|
202
|
+
headers = currentRowFields.map((_, i) => `col_${i + 1}`);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Construct Row object
|
|
206
|
+
const rowObj: Row = {};
|
|
207
|
+
for (let i = 0; i < headers.length; i++) {
|
|
208
|
+
const key = headers[i] || `col_${i + 1}`;
|
|
209
|
+
const val = currentRowFields[i] ?? "";
|
|
210
|
+
rowObj[key] = val;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
rowNumber++;
|
|
214
|
+
rowsInCurrentBatch.push(rowObj);
|
|
215
|
+
currentRowFields = [];
|
|
216
|
+
|
|
217
|
+
if (rowsInCurrentBatch.length >= batchSize) {
|
|
218
|
+
yield {
|
|
219
|
+
rows: rowsInCurrentBatch,
|
|
220
|
+
offset: globalRowOffset,
|
|
221
|
+
};
|
|
222
|
+
globalRowOffset += rowsInCurrentBatch.length;
|
|
223
|
+
rowsInCurrentBatch = [];
|
|
224
|
+
}
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
currentField += char;
|
|
229
|
+
cursor++;
|
|
230
|
+
byteOffset++;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Compact processed buffer to keep memory bounded
|
|
235
|
+
buffer = buffer.slice(cursor);
|
|
236
|
+
cursor = 0;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Process remainder if any
|
|
240
|
+
if (inQuotes) {
|
|
241
|
+
throw new ParseError("Unclosed quote at end of CSV stream", {
|
|
242
|
+
file: mergedOptions.filePath,
|
|
243
|
+
row: rowNumber + 1,
|
|
244
|
+
byteOffset,
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (currentField.length > 0 || currentRowFields.length > 0) {
|
|
249
|
+
currentRowFields.push(currentField);
|
|
250
|
+
|
|
251
|
+
if (hasHeader && headers === null) {
|
|
252
|
+
headers = currentRowFields.map((h, i) =>
|
|
253
|
+
h.trim().length > 0 ? h.trim() : `col_${i + 1}`
|
|
254
|
+
);
|
|
255
|
+
} else {
|
|
256
|
+
if (headers === null) {
|
|
257
|
+
headers = currentRowFields.map((_, i) => `col_${i + 1}`);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const rowObj: Row = {};
|
|
261
|
+
for (let i = 0; i < headers.length; i++) {
|
|
262
|
+
const key = headers[i] || `col_${i + 1}`;
|
|
263
|
+
const val = currentRowFields[i] ?? "";
|
|
264
|
+
rowObj[key] = val;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
rowNumber++;
|
|
268
|
+
rowsInCurrentBatch.push(rowObj);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if (rowsInCurrentBatch.length > 0) {
|
|
273
|
+
yield {
|
|
274
|
+
rows: rowsInCurrentBatch,
|
|
275
|
+
offset: globalRowOffset,
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
async inspect(options?: ReaderOptions): Promise<InspectionMetadata> {
|
|
281
|
+
let rowCount = 0;
|
|
282
|
+
let columnNames: string[] = [];
|
|
283
|
+
|
|
284
|
+
for await (const batch of this.read({ ...options, batchSize: 5000 })) {
|
|
285
|
+
if (columnNames.length === 0 && batch.rows.length > 0 && batch.rows[0]) {
|
|
286
|
+
columnNames = Object.keys(batch.rows[0]);
|
|
287
|
+
}
|
|
288
|
+
rowCount += batch.rows.length;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
return {
|
|
292
|
+
format: "CSV",
|
|
293
|
+
rowCount,
|
|
294
|
+
columnCount: columnNames.length,
|
|
295
|
+
columns: columnNames.map((name) => ({
|
|
296
|
+
name,
|
|
297
|
+
type: "string",
|
|
298
|
+
nullPercentage: 0,
|
|
299
|
+
})),
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import type { Readable } from "node:stream";
|
|
2
|
+
import { InvalidArgumentError } from "../core/errors.js";
|
|
3
|
+
import type { FormatAdapter, ReaderOptions, TabularReader } from "../core/types.js";
|
|
4
|
+
import { CSVReader } from "./csv.js";
|
|
5
|
+
import { JSONReader } from "./json.js";
|
|
6
|
+
import { JSONLReader } from "./jsonl.js";
|
|
7
|
+
import { XLSXReader } from "./xlsx.js";
|
|
8
|
+
|
|
9
|
+
export * from "./csv.js";
|
|
10
|
+
export * from "./json.js";
|
|
11
|
+
export * from "./jsonl.js";
|
|
12
|
+
export * from "./xlsx.js";
|
|
13
|
+
|
|
14
|
+
const adapters: Record<string, FormatAdapter> = {
|
|
15
|
+
csv: {
|
|
16
|
+
name: "CSV",
|
|
17
|
+
extensions: [".csv", ".tsv", ".txt"],
|
|
18
|
+
createReader: (input, options) => new CSVReader(input, options),
|
|
19
|
+
createWriter: () => {
|
|
20
|
+
throw new Error("Writer adapter called from reader");
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
json: {
|
|
24
|
+
name: "JSON",
|
|
25
|
+
extensions: [".json"],
|
|
26
|
+
createReader: (input, options) => new JSONReader(input, options),
|
|
27
|
+
createWriter: () => {
|
|
28
|
+
throw new Error("Writer adapter called from reader");
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
jsonl: {
|
|
32
|
+
name: "JSONL",
|
|
33
|
+
extensions: [".jsonl", ".ndjson", ".ldjson"],
|
|
34
|
+
createReader: (input, options) => new JSONLReader(input, options),
|
|
35
|
+
createWriter: () => {
|
|
36
|
+
throw new Error("Writer adapter called from reader");
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
xlsx: {
|
|
40
|
+
name: "XLSX",
|
|
41
|
+
extensions: [".xlsx", ".xlsm"],
|
|
42
|
+
createReader: (input, options) => new XLSXReader(input, options),
|
|
43
|
+
createWriter: () => {
|
|
44
|
+
throw new Error("Writer adapter called from reader");
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Infers format name from file path or extension.
|
|
51
|
+
*/
|
|
52
|
+
export function inferFormatFromPath(filePath: string): string | null {
|
|
53
|
+
const cleanPath = filePath.toLowerCase().replace(/\.gz$/, "");
|
|
54
|
+
for (const [format, adapter] of Object.entries(adapters)) {
|
|
55
|
+
if (adapter.extensions.some((ext) => cleanPath.endsWith(ext))) {
|
|
56
|
+
return format;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Creates an appropriate TabularReader for input and format.
|
|
64
|
+
*/
|
|
65
|
+
export function createReader(
|
|
66
|
+
input: Readable | string,
|
|
67
|
+
options: ReaderOptions & { format?: string } = {}
|
|
68
|
+
): TabularReader {
|
|
69
|
+
let format = options.format?.toLowerCase();
|
|
70
|
+
|
|
71
|
+
if (!format && typeof input === "string" && input !== "-") {
|
|
72
|
+
format = inferFormatFromPath(input) ?? undefined;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (!format) {
|
|
76
|
+
// Default to CSV if cannot infer
|
|
77
|
+
format = "csv";
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const adapter = adapters[format];
|
|
81
|
+
if (!adapter) {
|
|
82
|
+
throw new InvalidArgumentError(
|
|
83
|
+
`Unsupported input format: "${format}". Supported formats are: ${Object.keys(adapters).join(", ")}`
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return adapter.createReader(input, options);
|
|
88
|
+
}
|