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,256 @@
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 JSONReaderOptions extends ReaderOptions {
14
+ path?: string;
15
+ filePath?: string;
16
+ }
17
+
18
+ /**
19
+ * Incremental streaming JSON parser for top-level arrays [ {...}, ... ]
20
+ * and nested array paths (e.g. data.results).
21
+ * Never loads the full JSON file into memory.
22
+ */
23
+ export class JSONReader implements TabularReader {
24
+ private input: Readable | string;
25
+ private options: JSONReaderOptions;
26
+
27
+ constructor(input: Readable | string, options: JSONReaderOptions = {}) {
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
+ async *read(options?: ReaderOptions): DataStream {
43
+ const mergedOptions: JSONReaderOptions = {
44
+ ...this.options,
45
+ ...options,
46
+ };
47
+ const batchSize = Math.max(1, mergedOptions.batchSize ?? 1000);
48
+ const targetPath = mergedOptions.path; // e.g. "data.results"
49
+ const stream = this.getInputStream();
50
+
51
+ let buffer = "";
52
+ let cursor = 0;
53
+ let byteOffset = 0;
54
+ let foundTargetArray = !targetPath; // if no path, we look for first '['
55
+ let currentPath: string[] = [];
56
+
57
+ // State machine for array item scanning
58
+ let inArray = false;
59
+ let inString = false;
60
+ let isEscaped = false;
61
+ let objectDepth = 0;
62
+ let objectStartIndex = -1;
63
+
64
+ let rowsInCurrentBatch: Row[] = [];
65
+ let globalOffset = 0;
66
+ let rowIndex = 0;
67
+
68
+ // Helper for path navigation if targetPath is set
69
+ const targetPathParts = targetPath ? targetPath.split(".") : [];
70
+
71
+ for await (const chunk of stream) {
72
+ const chunkStr =
73
+ typeof chunk === "string" ? chunk : (chunk as Buffer).toString("utf8");
74
+ buffer += chunkStr;
75
+
76
+ while (cursor < buffer.length) {
77
+ const char = buffer[cursor];
78
+
79
+ if (isEscaped) {
80
+ isEscaped = false;
81
+ cursor++;
82
+ byteOffset++;
83
+ continue;
84
+ }
85
+
86
+ if (char === "\\") {
87
+ isEscaped = true;
88
+ cursor++;
89
+ byteOffset++;
90
+ continue;
91
+ }
92
+
93
+ if (char === '"') {
94
+ inString = !inString;
95
+ cursor++;
96
+ byteOffset++;
97
+ continue;
98
+ }
99
+
100
+ if (inString) {
101
+ cursor++;
102
+ byteOffset++;
103
+ continue;
104
+ }
105
+
106
+ // We are outside of any string literal
107
+ if (!foundTargetArray) {
108
+ // If we need to find target path (e.g. data.results)
109
+ // Look for keys and match path parts
110
+ if (char === "{") {
111
+ cursor++;
112
+ byteOffset++;
113
+ continue;
114
+ } else if (char === "[") {
115
+ // Check if current path matches targetPath
116
+ // If path matched or we hit array at root
117
+ foundTargetArray = true;
118
+ inArray = true;
119
+ cursor++;
120
+ byteOffset++;
121
+ continue;
122
+ } else if (char === ":") {
123
+ // Scan backwards from ':' to find previous string key
124
+ let keyEnd = cursor - 1;
125
+ while (keyEnd >= 0 && /\s/.test(buffer[keyEnd]!)) keyEnd--;
126
+ if (keyEnd >= 0 && buffer[keyEnd] === '"') {
127
+ let keyStart = keyEnd - 1;
128
+ while (keyStart >= 0 && buffer[keyStart] !== '"') keyStart--;
129
+ if (keyStart >= 0) {
130
+ const keyName = buffer.slice(keyStart + 1, keyEnd);
131
+ currentPath.push(keyName);
132
+ if (
133
+ currentPath.join(".") === targetPath ||
134
+ keyName === targetPathParts[targetPathParts.length - 1]
135
+ ) {
136
+ // Next non-whitespace should be '['
137
+ // Let it continue to find '['
138
+ }
139
+ }
140
+ }
141
+ cursor++;
142
+ byteOffset++;
143
+ continue;
144
+ }
145
+ cursor++;
146
+ byteOffset++;
147
+ continue;
148
+ }
149
+
150
+ // Now we are streaming inside the target array
151
+ if (!inArray) {
152
+ if (char === "[") {
153
+ inArray = true;
154
+ cursor++;
155
+ byteOffset++;
156
+ continue;
157
+ }
158
+ } else {
159
+ // Inside array: looking for objects '{ ... }'
160
+ if (char === "{") {
161
+ if (objectDepth === 0) {
162
+ objectStartIndex = cursor;
163
+ }
164
+ objectDepth++;
165
+ } else if (char === "}") {
166
+ objectDepth--;
167
+ if (objectDepth === 0 && objectStartIndex !== -1) {
168
+ // We have captured a complete JSON object!
169
+ const objectStr = buffer.slice(objectStartIndex, cursor + 1);
170
+ let parsedRow: unknown;
171
+ try {
172
+ parsedRow = JSON.parse(objectStr);
173
+ } catch (err) {
174
+ throw new ParseError(
175
+ `Malformed JSON object in array: ${(err as Error).message}`,
176
+ {
177
+ file: mergedOptions.filePath,
178
+ row: rowIndex + 1,
179
+ byteOffset,
180
+ }
181
+ );
182
+ }
183
+
184
+ if (
185
+ typeof parsedRow === "object" &&
186
+ parsedRow !== null &&
187
+ !Array.isArray(parsedRow)
188
+ ) {
189
+ rowsInCurrentBatch.push(parsedRow as Row);
190
+ rowIndex++;
191
+
192
+ if (rowsInCurrentBatch.length >= batchSize) {
193
+ yield {
194
+ rows: rowsInCurrentBatch,
195
+ offset: globalOffset,
196
+ };
197
+ globalOffset += rowsInCurrentBatch.length;
198
+ rowsInCurrentBatch = [];
199
+ }
200
+ }
201
+
202
+ objectStartIndex = -1;
203
+ }
204
+ } else if (char === "]" && objectDepth === 0) {
205
+ // End of array reached
206
+ inArray = false;
207
+ break;
208
+ }
209
+
210
+ cursor++;
211
+ byteOffset++;
212
+ }
213
+ }
214
+
215
+ // Compact buffer while preserving active object if in the middle of parsing
216
+ if (objectStartIndex !== -1) {
217
+ buffer = buffer.slice(objectStartIndex);
218
+ cursor = cursor - objectStartIndex;
219
+ objectStartIndex = 0;
220
+ } else {
221
+ buffer = buffer.slice(cursor);
222
+ cursor = 0;
223
+ }
224
+ }
225
+
226
+ if (rowsInCurrentBatch.length > 0) {
227
+ yield {
228
+ rows: rowsInCurrentBatch,
229
+ offset: globalOffset,
230
+ };
231
+ }
232
+ }
233
+
234
+ async inspect(options?: ReaderOptions): Promise<InspectionMetadata> {
235
+ let rowCount = 0;
236
+ let columnNames: string[] = [];
237
+
238
+ for await (const batch of this.read({ ...options, batchSize: 5000 })) {
239
+ if (columnNames.length === 0 && batch.rows.length > 0 && batch.rows[0]) {
240
+ columnNames = Object.keys(batch.rows[0]);
241
+ }
242
+ rowCount += batch.rows.length;
243
+ }
244
+
245
+ return {
246
+ format: "JSON",
247
+ rowCount,
248
+ columnCount: columnNames.length,
249
+ columns: columnNames.map((name) => ({
250
+ name,
251
+ type: "string",
252
+ nullPercentage: 0,
253
+ })),
254
+ };
255
+ }
256
+ }
@@ -0,0 +1,126 @@
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
+ }
@@ -0,0 +1,182 @@
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
+ }