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.
Files changed (225) hide show
  1. package/README.md +401 -0
  2. package/benchmarks/memory-bench.ts +109 -0
  3. package/dist/analytics/reduce.d.ts +33 -0
  4. package/dist/analytics/reduce.d.ts.map +1 -0
  5. package/dist/analytics/reduce.js +318 -0
  6. package/dist/analytics/reduce.js.map +1 -0
  7. package/dist/analytics/schema-inference.d.ts +31 -0
  8. package/dist/analytics/schema-inference.d.ts.map +1 -0
  9. package/dist/analytics/schema-inference.js +166 -0
  10. package/dist/analytics/schema-inference.js.map +1 -0
  11. package/dist/analytics/semantic-types.d.ts +6 -0
  12. package/dist/analytics/semantic-types.d.ts.map +1 -0
  13. package/dist/analytics/semantic-types.js +40 -0
  14. package/dist/analytics/semantic-types.js.map +1 -0
  15. package/dist/analytics/stats.d.ts +102 -0
  16. package/dist/analytics/stats.d.ts.map +1 -0
  17. package/dist/analytics/stats.js +307 -0
  18. package/dist/analytics/stats.js.map +1 -0
  19. package/dist/analytics/validator.d.ts +32 -0
  20. package/dist/analytics/validator.d.ts.map +1 -0
  21. package/dist/analytics/validator.js +96 -0
  22. package/dist/analytics/validator.js.map +1 -0
  23. package/dist/cli/commands/cast.d.ts +13 -0
  24. package/dist/cli/commands/cast.d.ts.map +1 -0
  25. package/dist/cli/commands/cast.js +39 -0
  26. package/dist/cli/commands/cast.js.map +1 -0
  27. package/dist/cli/commands/convert.d.ts +15 -0
  28. package/dist/cli/commands/convert.d.ts.map +1 -0
  29. package/dist/cli/commands/convert.js +89 -0
  30. package/dist/cli/commands/convert.js.map +1 -0
  31. package/dist/cli/commands/filter.d.ts +11 -0
  32. package/dist/cli/commands/filter.d.ts.map +1 -0
  33. package/dist/cli/commands/filter.js +38 -0
  34. package/dist/cli/commands/filter.js.map +1 -0
  35. package/dist/cli/commands/inspect.d.ts +10 -0
  36. package/dist/cli/commands/inspect.d.ts.map +1 -0
  37. package/dist/cli/commands/inspect.js +134 -0
  38. package/dist/cli/commands/inspect.js.map +1 -0
  39. package/dist/cli/commands/map.d.ts +11 -0
  40. package/dist/cli/commands/map.d.ts.map +1 -0
  41. package/dist/cli/commands/map.js +43 -0
  42. package/dist/cli/commands/map.js.map +1 -0
  43. package/dist/cli/commands/reduce.d.ts +12 -0
  44. package/dist/cli/commands/reduce.d.ts.map +1 -0
  45. package/dist/cli/commands/reduce.js +49 -0
  46. package/dist/cli/commands/reduce.js.map +1 -0
  47. package/dist/cli/commands/rename.d.ts +11 -0
  48. package/dist/cli/commands/rename.d.ts.map +1 -0
  49. package/dist/cli/commands/rename.js +39 -0
  50. package/dist/cli/commands/rename.js.map +1 -0
  51. package/dist/cli/commands/sample.d.ts +13 -0
  52. package/dist/cli/commands/sample.d.ts.map +1 -0
  53. package/dist/cli/commands/sample.js +40 -0
  54. package/dist/cli/commands/sample.js.map +1 -0
  55. package/dist/cli/commands/schema.d.ts +13 -0
  56. package/dist/cli/commands/schema.d.ts.map +1 -0
  57. package/dist/cli/commands/schema.js +57 -0
  58. package/dist/cli/commands/schema.js.map +1 -0
  59. package/dist/cli/commands/select.d.ts +11 -0
  60. package/dist/cli/commands/select.d.ts.map +1 -0
  61. package/dist/cli/commands/select.js +39 -0
  62. package/dist/cli/commands/select.js.map +1 -0
  63. package/dist/cli/commands/stats.d.ts +15 -0
  64. package/dist/cli/commands/stats.d.ts.map +1 -0
  65. package/dist/cli/commands/stats.js +75 -0
  66. package/dist/cli/commands/stats.js.map +1 -0
  67. package/dist/cli/commands/validate.d.ts +12 -0
  68. package/dist/cli/commands/validate.d.ts.map +1 -0
  69. package/dist/cli/commands/validate.js +86 -0
  70. package/dist/cli/commands/validate.js.map +1 -0
  71. package/dist/cli/index.d.ts +3 -0
  72. package/dist/cli/index.d.ts.map +1 -0
  73. package/dist/cli/index.js +262 -0
  74. package/dist/cli/index.js.map +1 -0
  75. package/dist/core/batch.d.ts +15 -0
  76. package/dist/core/batch.d.ts.map +1 -0
  77. package/dist/core/batch.js +67 -0
  78. package/dist/core/batch.js.map +1 -0
  79. package/dist/core/errors.d.ts +35 -0
  80. package/dist/core/errors.d.ts.map +1 -0
  81. package/dist/core/errors.js +55 -0
  82. package/dist/core/errors.js.map +1 -0
  83. package/dist/core/pipeline.d.ts +40 -0
  84. package/dist/core/pipeline.d.ts.map +1 -0
  85. package/dist/core/pipeline.js +120 -0
  86. package/dist/core/pipeline.js.map +1 -0
  87. package/dist/core/types.d.ts +73 -0
  88. package/dist/core/types.d.ts.map +1 -0
  89. package/dist/core/types.js +2 -0
  90. package/dist/core/types.js.map +1 -0
  91. package/dist/index.d.ts +22 -0
  92. package/dist/index.d.ts.map +1 -0
  93. package/dist/index.js +28 -0
  94. package/dist/index.js.map +1 -0
  95. package/dist/readers/csv.d.ts +24 -0
  96. package/dist/readers/csv.d.ts.map +1 -0
  97. package/dist/readers/csv.js +251 -0
  98. package/dist/readers/csv.js.map +1 -0
  99. package/dist/readers/index.d.ts +17 -0
  100. package/dist/readers/index.d.ts.map +1 -0
  101. package/dist/readers/index.js +74 -0
  102. package/dist/readers/index.js.map +1 -0
  103. package/dist/readers/json.d.ts +20 -0
  104. package/dist/readers/json.d.ts.map +1 -0
  105. package/dist/readers/json.js +219 -0
  106. package/dist/readers/json.js.map +1 -0
  107. package/dist/readers/jsonl.d.ts +14 -0
  108. package/dist/readers/jsonl.d.ts.map +1 -0
  109. package/dist/readers/jsonl.js +95 -0
  110. package/dist/readers/jsonl.js.map +1 -0
  111. package/dist/readers/xlsx.d.ts +18 -0
  112. package/dist/readers/xlsx.d.ts.map +1 -0
  113. package/dist/readers/xlsx.js +138 -0
  114. package/dist/readers/xlsx.js.map +1 -0
  115. package/dist/transforms/cast.d.ts +21 -0
  116. package/dist/transforms/cast.d.ts.map +1 -0
  117. package/dist/transforms/cast.js +182 -0
  118. package/dist/transforms/cast.js.map +1 -0
  119. package/dist/transforms/expression.d.ts +67 -0
  120. package/dist/transforms/expression.d.ts.map +1 -0
  121. package/dist/transforms/expression.js +943 -0
  122. package/dist/transforms/expression.js.map +1 -0
  123. package/dist/transforms/filter.d.ts +6 -0
  124. package/dist/transforms/filter.d.ts.map +1 -0
  125. package/dist/transforms/filter.js +31 -0
  126. package/dist/transforms/filter.js.map +1 -0
  127. package/dist/transforms/map.d.ts +12 -0
  128. package/dist/transforms/map.d.ts.map +1 -0
  129. package/dist/transforms/map.js +76 -0
  130. package/dist/transforms/map.js.map +1 -0
  131. package/dist/transforms/rename.d.ts +10 -0
  132. package/dist/transforms/rename.d.ts.map +1 -0
  133. package/dist/transforms/rename.js +39 -0
  134. package/dist/transforms/rename.js.map +1 -0
  135. package/dist/transforms/sample.d.ts +10 -0
  136. package/dist/transforms/sample.d.ts.map +1 -0
  137. package/dist/transforms/sample.js +47 -0
  138. package/dist/transforms/sample.js.map +1 -0
  139. package/dist/transforms/select.d.ts +6 -0
  140. package/dist/transforms/select.d.ts.map +1 -0
  141. package/dist/transforms/select.js +63 -0
  142. package/dist/transforms/select.js.map +1 -0
  143. package/dist/utils/compression.d.ts +14 -0
  144. package/dist/utils/compression.d.ts.map +1 -0
  145. package/dist/utils/compression.js +38 -0
  146. package/dist/utils/compression.js.map +1 -0
  147. package/dist/utils/formatting.d.ts +6 -0
  148. package/dist/utils/formatting.d.ts.map +1 -0
  149. package/dist/utils/formatting.js +56 -0
  150. package/dist/utils/formatting.js.map +1 -0
  151. package/dist/utils/progress.d.ts +13 -0
  152. package/dist/utils/progress.d.ts.map +1 -0
  153. package/dist/utils/progress.js +27 -0
  154. package/dist/utils/progress.js.map +1 -0
  155. package/dist/writers/csv.d.ts +18 -0
  156. package/dist/writers/csv.d.ts.map +1 -0
  157. package/dist/writers/csv.js +84 -0
  158. package/dist/writers/csv.js.map +1 -0
  159. package/dist/writers/index.d.ts +17 -0
  160. package/dist/writers/index.d.ts.map +1 -0
  161. package/dist/writers/index.js +73 -0
  162. package/dist/writers/index.js.map +1 -0
  163. package/dist/writers/json.d.ts +12 -0
  164. package/dist/writers/json.d.ts.map +1 -0
  165. package/dist/writers/json.js +42 -0
  166. package/dist/writers/json.js.map +1 -0
  167. package/dist/writers/jsonl.d.ts +12 -0
  168. package/dist/writers/jsonl.d.ts.map +1 -0
  169. package/dist/writers/jsonl.js +41 -0
  170. package/dist/writers/jsonl.js.map +1 -0
  171. package/dist/writers/xlsx.d.ts +13 -0
  172. package/dist/writers/xlsx.d.ts.map +1 -0
  173. package/dist/writers/xlsx.js +65 -0
  174. package/dist/writers/xlsx.js.map +1 -0
  175. package/package.json +44 -0
  176. package/src/analytics/reduce.ts +388 -0
  177. package/src/analytics/schema-inference.ts +217 -0
  178. package/src/analytics/semantic-types.ts +36 -0
  179. package/src/analytics/stats.ts +383 -0
  180. package/src/analytics/validator.ts +134 -0
  181. package/src/cli/commands/cast.ts +60 -0
  182. package/src/cli/commands/convert.ts +128 -0
  183. package/src/cli/commands/filter.ts +58 -0
  184. package/src/cli/commands/inspect.ts +193 -0
  185. package/src/cli/commands/map.ts +66 -0
  186. package/src/cli/commands/reduce.ts +74 -0
  187. package/src/cli/commands/rename.ts +59 -0
  188. package/src/cli/commands/sample.ts +61 -0
  189. package/src/cli/commands/schema.ts +86 -0
  190. package/src/cli/commands/select.ts +59 -0
  191. package/src/cli/commands/stats.ts +100 -0
  192. package/src/cli/commands/validate.ts +123 -0
  193. package/src/cli/index.ts +285 -0
  194. package/src/core/batch.ts +81 -0
  195. package/src/core/errors.ts +81 -0
  196. package/src/core/pipeline.ts +150 -0
  197. package/src/core/types.ts +110 -0
  198. package/src/index.ts +52 -0
  199. package/src/readers/csv.ts +302 -0
  200. package/src/readers/index.ts +88 -0
  201. package/src/readers/json.ts +256 -0
  202. package/src/readers/jsonl.ts +126 -0
  203. package/src/readers/xlsx.ts +182 -0
  204. package/src/transforms/cast.ts +212 -0
  205. package/src/transforms/expression.ts +1114 -0
  206. package/src/transforms/filter.ts +38 -0
  207. package/src/transforms/map.ts +99 -0
  208. package/src/transforms/rename.ts +47 -0
  209. package/src/transforms/sample.ts +57 -0
  210. package/src/transforms/select.ts +64 -0
  211. package/src/utils/compression.ts +43 -0
  212. package/src/utils/formatting.ts +75 -0
  213. package/src/utils/progress.ts +37 -0
  214. package/src/writers/csv.ts +106 -0
  215. package/src/writers/index.ts +87 -0
  216. package/src/writers/json.ts +52 -0
  217. package/src/writers/jsonl.ts +49 -0
  218. package/src/writers/xlsx.ts +87 -0
  219. package/tests/analytics.test.ts +119 -0
  220. package/tests/cli-integration.test.ts +187 -0
  221. package/tests/map-reduce.test.ts +223 -0
  222. package/tests/readers-writers.test.ts +189 -0
  223. package/tests/transforms.test.ts +199 -0
  224. package/tsconfig.json +21 -0
  225. 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
+ }