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,38 @@
1
+ import type { DataStream, Row, TransformFunction } from "../core/types.js";
2
+ import { compileExpression } from "./expression.js";
3
+
4
+ /**
5
+ * Creates a transform that filters rows based on a safe compiled filter expression.
6
+ */
7
+ export function filterRows(
8
+ predicateOrExpression: string | ((row: Row) => boolean)
9
+ ): TransformFunction {
10
+ const predicate =
11
+ typeof predicateOrExpression === "function"
12
+ ? predicateOrExpression
13
+ : compileExpression(predicateOrExpression);
14
+
15
+ return function (stream: DataStream): DataStream {
16
+ return (async function* () {
17
+ for await (const batch of stream) {
18
+ const rows = batch.rows;
19
+ const len = rows.length;
20
+ const filteredRows: Row[] = [];
21
+
22
+ for (let i = 0; i < len; i++) {
23
+ const row = rows[i]!;
24
+ if (predicate(row)) {
25
+ filteredRows.push(row);
26
+ }
27
+ }
28
+
29
+ if (filteredRows.length > 0) {
30
+ yield {
31
+ rows: filteredRows,
32
+ offset: batch.offset,
33
+ };
34
+ }
35
+ }
36
+ })();
37
+ };
38
+ }
@@ -0,0 +1,99 @@
1
+ import { InvalidArgumentError } from "../core/errors.js";
2
+ import type { DataBatch, DataStream, Row, TransformFunction } from "../core/types.js";
3
+ import { compileValueExpression } from "./expression.js";
4
+
5
+ /**
6
+ * Parses CLI map argument specs into a column-to-expression dictionary.
7
+ * Supports formats: "profit = revenue - cost", "tax=revenue*0.2"
8
+ */
9
+ export function parseMapSpecs(specs: string[]): Record<string, string> {
10
+ const result: Record<string, string> = {};
11
+
12
+ for (const spec of specs) {
13
+ const trimmed = spec.trim();
14
+ if (!trimmed) continue;
15
+
16
+ const eqIdx = trimmed.indexOf("=");
17
+ if (eqIdx === -1) {
18
+ throw new InvalidArgumentError(
19
+ `Invalid map specification "${spec}". Expected format "column_name = expression"`
20
+ );
21
+ }
22
+
23
+ const colName = trimmed.slice(0, eqIdx).trim();
24
+ const expr = trimmed.slice(eqIdx + 1).trim();
25
+
26
+ if (!colName) {
27
+ throw new InvalidArgumentError(
28
+ `Missing target column name in map specification "${spec}"`
29
+ );
30
+ }
31
+ if (!expr) {
32
+ throw new InvalidArgumentError(
33
+ `Missing expression in map specification "${spec}"`
34
+ );
35
+ }
36
+
37
+ result[colName] = expr;
38
+ }
39
+
40
+ return result;
41
+ }
42
+
43
+ export type RowMapper = (row: Row) => Row;
44
+
45
+ /**
46
+ * Creates a high-performance streaming transform that computes/derives columns per row.
47
+ */
48
+ export function mapRows(
49
+ specs: Record<string, string> | RowMapper
50
+ ): TransformFunction {
51
+ if (typeof specs === "function") {
52
+ const customMapper = specs;
53
+ return async function* (stream: DataStream): DataStream {
54
+ for await (const batch of stream) {
55
+ const len = batch.rows.length;
56
+ const mappedRows: Row[] = new Array(len);
57
+ for (let i = 0; i < len; i++) {
58
+ mappedRows[i] = customMapper(batch.rows[i]!);
59
+ }
60
+ yield {
61
+ rows: mappedRows,
62
+ offset: batch.offset,
63
+ };
64
+ }
65
+ };
66
+ }
67
+
68
+ // Pre-compile all AST expression evaluators once before processing stream
69
+ const compiledEntries = Object.entries(specs).map(([targetCol, exprStr]) => ({
70
+ targetCol,
71
+ evaluator: compileValueExpression(exprStr),
72
+ }));
73
+
74
+ return async function* (stream: DataStream): DataStream {
75
+ for await (const batch of stream) {
76
+ const rowCount = batch.rows.length;
77
+ const mappedRows: Row[] = new Array(rowCount);
78
+ const compiledLen = compiledEntries.length;
79
+
80
+ for (let i = 0; i < rowCount; i++) {
81
+ const srcRow = batch.rows[i]!;
82
+ // Clone row to avoid mutating original
83
+ const outRow: Row = { ...srcRow };
84
+
85
+ for (let j = 0; j < compiledLen; j++) {
86
+ const entry = compiledEntries[j]!;
87
+ outRow[entry.targetCol] = entry.evaluator(srcRow);
88
+ }
89
+
90
+ mappedRows[i] = outRow;
91
+ }
92
+
93
+ yield {
94
+ rows: mappedRows,
95
+ offset: batch.offset,
96
+ };
97
+ }
98
+ };
99
+ }
@@ -0,0 +1,47 @@
1
+ import type { DataStream, Row, TransformFunction } from "../core/types.js";
2
+
3
+ /**
4
+ * Creates a transform that renames specified columns in streaming batches.
5
+ */
6
+ export function renameColumns(
7
+ mapping: Record<string, string>
8
+ ): TransformFunction {
9
+ return function (stream: DataStream): DataStream {
10
+ return (async function* () {
11
+ for await (const batch of stream) {
12
+ const renamedRows: Row[] = new Array(batch.rows.length);
13
+
14
+ for (let i = 0; i < batch.rows.length; i++) {
15
+ const row = batch.rows[i]!;
16
+ const newRow: Row = {};
17
+
18
+ for (const [key, val] of Object.entries(row)) {
19
+ const targetKey = mapping[key] || key;
20
+ newRow[targetKey] = val;
21
+ }
22
+
23
+ renamedRows[i] = newRow;
24
+ }
25
+
26
+ yield {
27
+ rows: renamedRows,
28
+ offset: batch.offset,
29
+ };
30
+ }
31
+ })();
32
+ };
33
+ }
34
+
35
+ /**
36
+ * Parses command line rename specs like ["username=name", "signup_date=created_at"].
37
+ */
38
+ export function parseRenameSpecs(specs: string[]): Record<string, string> {
39
+ const mapping: Record<string, string> = {};
40
+ for (const spec of specs) {
41
+ const parts = spec.split("=");
42
+ if (parts.length === 2 && parts[0] && parts[1]) {
43
+ mapping[parts[0].trim()] = parts[1].trim();
44
+ }
45
+ }
46
+ return mapping;
47
+ }
@@ -0,0 +1,57 @@
1
+ import type { DataStream, Row, TransformFunction } from "../core/types.js";
2
+
3
+ /**
4
+ * Creates a deterministic PRNG using Mulberry32 algorithm.
5
+ */
6
+ function createPrng(seed: number): () => number {
7
+ let s = seed >>> 0;
8
+ return function () {
9
+ let t = (s += 0x6d2b79f5);
10
+ t = Math.imul(t ^ (t >>> 15), t | 1);
11
+ t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
12
+ return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
13
+ };
14
+ }
15
+
16
+ export interface SampleOptions {
17
+ rows: number;
18
+ seed?: number;
19
+ }
20
+
21
+ /**
22
+ * Reservoir sampling transform with bounded O(k) memory.
23
+ */
24
+ export function sampleRows(options: SampleOptions): TransformFunction {
25
+ const k = Math.max(1, options.rows);
26
+ const randomFn =
27
+ options.seed !== undefined ? createPrng(options.seed) : Math.random;
28
+
29
+ return function (stream: DataStream): DataStream {
30
+ return (async function* () {
31
+ const reservoir: Row[] = [];
32
+ let totalRowsSeen = 0;
33
+
34
+ for await (const batch of stream) {
35
+ for (const row of batch.rows) {
36
+ if (totalRowsSeen < k) {
37
+ reservoir.push(row);
38
+ } else {
39
+ // Pick a random integer from 0 to totalRowsSeen
40
+ const j = Math.floor(randomFn() * (totalRowsSeen + 1));
41
+ if (j < k) {
42
+ reservoir[j] = row;
43
+ }
44
+ }
45
+ totalRowsSeen++;
46
+ }
47
+ }
48
+
49
+ if (reservoir.length > 0) {
50
+ yield {
51
+ rows: reservoir,
52
+ offset: 0,
53
+ };
54
+ }
55
+ })();
56
+ };
57
+ }
@@ -0,0 +1,64 @@
1
+ import type { DataBatch, DataStream, Row, TransformFunction } from "../core/types.js";
2
+
3
+ /**
4
+ * Creates a transform that projects only the specified columns in order.
5
+ */
6
+ export function selectColumns(columns: string[]): TransformFunction {
7
+ const columnSet = columns.map((c) => c.trim()).filter((c) => c.length > 0);
8
+ const len = columnSet.length;
9
+
10
+ let projectRow: (row: Row) => Row;
11
+ if (len === 1) {
12
+ const c0 = columnSet[0]!;
13
+ projectRow = (row) => ({ [c0]: row[c0] !== undefined ? row[c0] : "" });
14
+ } else if (len === 2) {
15
+ const c0 = columnSet[0]!, c1 = columnSet[1]!;
16
+ projectRow = (row) => ({
17
+ [c0]: row[c0] !== undefined ? row[c0] : "",
18
+ [c1]: row[c1] !== undefined ? row[c1] : "",
19
+ });
20
+ } else if (len === 3) {
21
+ const c0 = columnSet[0]!, c1 = columnSet[1]!, c2 = columnSet[2]!;
22
+ projectRow = (row) => ({
23
+ [c0]: row[c0] !== undefined ? row[c0] : "",
24
+ [c1]: row[c1] !== undefined ? row[c1] : "",
25
+ [c2]: row[c2] !== undefined ? row[c2] : "",
26
+ });
27
+ } else if (len === 4) {
28
+ const c0 = columnSet[0]!, c1 = columnSet[1]!, c2 = columnSet[2]!, c3 = columnSet[3]!;
29
+ projectRow = (row) => ({
30
+ [c0]: row[c0] !== undefined ? row[c0] : "",
31
+ [c1]: row[c1] !== undefined ? row[c1] : "",
32
+ [c2]: row[c2] !== undefined ? row[c2] : "",
33
+ [c3]: row[c3] !== undefined ? row[c3] : "",
34
+ });
35
+ } else {
36
+ projectRow = (row) => {
37
+ const newRow: Row = {};
38
+ for (let j = 0; j < len; j++) {
39
+ const col = columnSet[j]!;
40
+ newRow[col] = row[col] !== undefined ? row[col] : "";
41
+ }
42
+ return newRow;
43
+ };
44
+ }
45
+
46
+ return function (stream: DataStream): DataStream {
47
+ return (async function* () {
48
+ for await (const batch of stream) {
49
+ const rows = batch.rows;
50
+ const rowCount = rows.length;
51
+ const projectedRows: Row[] = new Array(rowCount);
52
+
53
+ for (let i = 0; i < rowCount; i++) {
54
+ projectedRows[i] = projectRow(rows[i]!);
55
+ }
56
+
57
+ yield {
58
+ rows: projectedRows,
59
+ offset: batch.offset,
60
+ };
61
+ }
62
+ })();
63
+ };
64
+ }
@@ -0,0 +1,43 @@
1
+ import { createReadStream, createWriteStream } from "node:fs";
2
+ import type { Readable, Writable } from "node:stream";
3
+ import { createGzip, createGunzip } from "node:zlib";
4
+
5
+ /**
6
+ * Checks if a path or filename indicates gzip compression.
7
+ */
8
+ export function isGzipPath(filePath: string): boolean {
9
+ return filePath.toLowerCase().endsWith(".gz");
10
+ }
11
+
12
+ /**
13
+ * Creates a readable stream, automatically piping through gunzip if the path ends with .gz.
14
+ */
15
+ export function openReadableStream(filePath: string): Readable {
16
+ if (filePath === "-") {
17
+ return process.stdin;
18
+ }
19
+
20
+ const rawStream = createReadStream(filePath);
21
+ if (isGzipPath(filePath)) {
22
+ const gunzip = createGunzip();
23
+ return rawStream.pipe(gunzip);
24
+ }
25
+ return rawStream;
26
+ }
27
+
28
+ /**
29
+ * Creates a writable stream, automatically piping through gzip if the path ends with .gz.
30
+ */
31
+ export function openWritableStream(filePath: string): Writable {
32
+ if (filePath === "-") {
33
+ return process.stdout;
34
+ }
35
+
36
+ const fileStream = createWriteStream(filePath);
37
+ if (isGzipPath(filePath)) {
38
+ const gzip = createGzip();
39
+ gzip.pipe(fileStream);
40
+ return gzip;
41
+ }
42
+ return fileStream;
43
+ }
@@ -0,0 +1,75 @@
1
+ export function formatNumber(num: number | null | undefined): string {
2
+ if (num === null || num === undefined || Number.isNaN(num)) {
3
+ return "-";
4
+ }
5
+ return new Intl.NumberFormat("en-US").format(num);
6
+ }
7
+
8
+ export function formatDecimal(num: number | null | undefined, digits = 2): string {
9
+ if (num === null || num === undefined || Number.isNaN(num)) {
10
+ return "-";
11
+ }
12
+ return new Intl.NumberFormat("en-US", {
13
+ minimumFractionDigits: 0,
14
+ maximumFractionDigits: digits,
15
+ }).format(num);
16
+ }
17
+
18
+ export function formatBytes(bytes: number | null | undefined): string {
19
+ if (bytes === null || bytes === undefined || bytes === 0) return "0 B";
20
+ const k = 1024;
21
+ const sizes = ["B", "KB", "MB", "GB", "TB"];
22
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
23
+ return `${Number.parseFloat((bytes / k ** i).toFixed(2))} ${sizes[i]}`;
24
+ }
25
+
26
+ export function formatTable(
27
+ headers: string[],
28
+ rows: string[][],
29
+ alignments?: Array<"left" | "right">
30
+ ): string {
31
+ if (headers.length === 0) return "";
32
+
33
+ const colWidths = headers.map((h) => h.length);
34
+
35
+ for (const row of rows) {
36
+ for (let i = 0; i < headers.length; i++) {
37
+ const cell = row[i] || "";
38
+ if (cell.length > (colWidths[i] || 0)) {
39
+ colWidths[i] = cell.length;
40
+ }
41
+ }
42
+ }
43
+
44
+ const formatCell = (text: string, width: number, align: "left" | "right") => {
45
+ if (align === "right") {
46
+ return text.padStart(width);
47
+ }
48
+ return text.padEnd(width);
49
+ };
50
+
51
+ const headerLine = headers
52
+ .map((h, i) =>
53
+ formatCell(h, colWidths[i]!, alignments?.[i] || "left")
54
+ )
55
+ .join(" ");
56
+
57
+ const rowLines = rows.map((row) =>
58
+ headers
59
+ .map((_, i) =>
60
+ formatCell(row[i] || "", colWidths[i]!, alignments?.[i] || "left")
61
+ )
62
+ .join(" ")
63
+ );
64
+
65
+ return [headerLine, ...rowLines].join("\n");
66
+ }
67
+
68
+ export function logMemoryDebug(): void {
69
+ if (process.env["ROWPIPE_DEBUG_MEMORY"] === "1") {
70
+ const mem = process.memoryUsage();
71
+ process.stderr.write(
72
+ `\n[ROWPIPE DEBUG MEMORY]\n Peak RSS: ${formatBytes(mem.rss)}\n Heap Used: ${formatBytes(mem.heapUsed)}\n Heap Total: ${formatBytes(mem.heapTotal)}\n External: ${formatBytes(mem.external)}\n\n`
73
+ );
74
+ }
75
+ }
@@ -0,0 +1,37 @@
1
+ import type { ProgressCallbackInfo } from "../core/types.js";
2
+ import { formatNumber } from "./formatting.js";
3
+
4
+ export interface ProgressReporterOptions {
5
+ quiet?: boolean;
6
+ noProgress?: boolean;
7
+ }
8
+
9
+ export class ProgressReporter {
10
+ private isEnabled: boolean;
11
+ private hasReported = false;
12
+
13
+ constructor(options: ProgressReporterOptions = {}) {
14
+ this.isEnabled =
15
+ !options.quiet &&
16
+ !options.noProgress &&
17
+ Boolean(process.stderr.isTTY);
18
+ }
19
+
20
+ update(info: ProgressCallbackInfo): void {
21
+ if (!this.isEnabled) return;
22
+ this.hasReported = true;
23
+
24
+ const rows = formatNumber(info.rowsProcessed);
25
+ const speed = formatNumber(info.rowsPerSecond);
26
+ const timeSec = (info.elapsedMs / 1000).toFixed(1);
27
+
28
+ const line = `\r\x1b[2K Processed ${rows} rows | ${timeSec}s | ${speed} rows/s`;
29
+ process.stderr.write(line);
30
+ }
31
+
32
+ done(): void {
33
+ if (this.isEnabled && this.hasReported) {
34
+ process.stderr.write("\r\x1b[2K");
35
+ }
36
+ }
37
+ }
@@ -0,0 +1,106 @@
1
+ import { createWriteStream } from "node:fs";
2
+ import type { Writable } from "node:stream";
3
+ import type { DataStream, TabularWriter, WriterOptions } from "../core/types.js";
4
+
5
+ export interface CSVWriterOptions extends WriterOptions {
6
+ delimiter?: string;
7
+ header?: boolean;
8
+ }
9
+
10
+ export class CSVWriter implements TabularWriter {
11
+ private output: Writable | string;
12
+ private options: CSVWriterOptions;
13
+ private createdStream?: Writable;
14
+
15
+ constructor(output: Writable | string, options: CSVWriterOptions = {}) {
16
+ this.output = output;
17
+ this.options = { ...options };
18
+ }
19
+
20
+ private getOutputStream(): Writable {
21
+ if (typeof this.output === "string") {
22
+ this.createdStream = createWriteStream(this.output);
23
+ return this.createdStream;
24
+ }
25
+ return this.output;
26
+ }
27
+
28
+ private escapeField(value: unknown, delimiter: string): string {
29
+ if (value === null || value === undefined) {
30
+ return "";
31
+ }
32
+ const str = String(value);
33
+ const mustQuote =
34
+ str.includes(delimiter) ||
35
+ str.includes('"') ||
36
+ str.includes("\n") ||
37
+ str.includes("\r");
38
+
39
+ if (mustQuote) {
40
+ return `"${str.replace(/"/g, '""')}"`;
41
+ }
42
+ return str;
43
+ }
44
+
45
+ private async writeChunk(stream: Writable, data: string): Promise<void> {
46
+ if (!stream.write(data)) {
47
+ await new Promise<void>((resolve) => stream.once("drain", resolve));
48
+ }
49
+ }
50
+
51
+ async write(dataStream: DataStream, options?: WriterOptions): Promise<void> {
52
+ const mergedOptions: CSVWriterOptions = {
53
+ ...this.options,
54
+ ...options,
55
+ };
56
+ const delimiter = mergedOptions.delimiter ?? ",";
57
+ const writeHeader = mergedOptions.header !== false;
58
+
59
+ const outStream = this.getOutputStream();
60
+ let headers: string[] | null = null;
61
+
62
+ for await (const batch of dataStream) {
63
+ if (batch.rows.length === 0) continue;
64
+
65
+ if (headers === null) {
66
+ // Collect all distinct keys from first batch to form header
67
+ const keySet = new Set<string>();
68
+ for (const row of batch.rows) {
69
+ for (const key of Object.keys(row)) {
70
+ keySet.add(key);
71
+ }
72
+ }
73
+ headers = Array.from(keySet);
74
+
75
+ if (writeHeader && headers.length > 0) {
76
+ const headerLine =
77
+ headers.map((h) => this.escapeField(h, delimiter)).join(delimiter) +
78
+ "\n";
79
+ await this.writeChunk(outStream, headerLine);
80
+ }
81
+ }
82
+
83
+ let chunkText = "";
84
+ for (const row of batch.rows) {
85
+ const line =
86
+ headers
87
+ .map((h) => this.escapeField(row[h], delimiter))
88
+ .join(delimiter) + "\n";
89
+ chunkText += line;
90
+ }
91
+
92
+ if (chunkText.length > 0) {
93
+ await this.writeChunk(outStream, chunkText);
94
+ }
95
+ }
96
+ }
97
+
98
+ async close(): Promise<void> {
99
+ if (this.createdStream) {
100
+ await new Promise<void>((resolve, reject) => {
101
+ this.createdStream!.end(() => resolve());
102
+ this.createdStream!.on("error", reject);
103
+ });
104
+ }
105
+ }
106
+ }
@@ -0,0 +1,87 @@
1
+ import type { Writable } from "node:stream";
2
+ import { InvalidArgumentError } from "../core/errors.js";
3
+ import type { FormatAdapter, TabularWriter, WriterOptions } from "../core/types.js";
4
+ import { CSVWriter } from "./csv.js";
5
+ import { JSONWriter } from "./json.js";
6
+ import { JSONLWriter } from "./jsonl.js";
7
+ import { XLSXWriter } 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: () => {
19
+ throw new Error("Reader adapter called from writer");
20
+ },
21
+ createWriter: (output, options) => new CSVWriter(output, options),
22
+ },
23
+ json: {
24
+ name: "JSON",
25
+ extensions: [".json"],
26
+ createReader: () => {
27
+ throw new Error("Reader adapter called from writer");
28
+ },
29
+ createWriter: (output) => new JSONWriter(output),
30
+ },
31
+ jsonl: {
32
+ name: "JSONL",
33
+ extensions: [".jsonl", ".ndjson", ".ldjson"],
34
+ createReader: () => {
35
+ throw new Error("Reader adapter called from writer");
36
+ },
37
+ createWriter: (output) => new JSONLWriter(output),
38
+ },
39
+ xlsx: {
40
+ name: "XLSX",
41
+ extensions: [".xlsx"],
42
+ createReader: () => {
43
+ throw new Error("Reader adapter called from writer");
44
+ },
45
+ createWriter: (output, options) => new XLSXWriter(output, options),
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 TabularWriter for output and format.
64
+ */
65
+ export function createWriter(
66
+ output: Writable | string,
67
+ options: WriterOptions & { format?: string } = {}
68
+ ): TabularWriter {
69
+ let format = options.format?.toLowerCase();
70
+
71
+ if (!format && typeof output === "string" && output !== "-") {
72
+ format = inferFormatFromPath(output) ?? undefined;
73
+ }
74
+
75
+ if (!format) {
76
+ format = "csv";
77
+ }
78
+
79
+ const adapter = adapters[format];
80
+ if (!adapter) {
81
+ throw new InvalidArgumentError(
82
+ `Unsupported output format: "${format}". Supported formats are: ${Object.keys(adapters).join(", ")}`
83
+ );
84
+ }
85
+
86
+ return adapter.createWriter(output, options);
87
+ }