rowpipe 1.0.0 → 1.2.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 +327 -41
- package/dist/cli/commands/diff.d.ts +30 -0
- package/dist/cli/commands/diff.d.ts.map +1 -0
- package/dist/cli/commands/diff.js +143 -0
- package/dist/cli/commands/diff.js.map +1 -0
- package/dist/cli/commands/files.d.ts +35 -0
- package/dist/cli/commands/files.d.ts.map +1 -0
- package/dist/cli/commands/files.js +93 -0
- package/dist/cli/commands/files.js.map +1 -0
- package/dist/cli/index.js +89 -5
- package/dist/cli/index.js.map +1 -1
- package/dist/core/errors.d.ts +23 -0
- package/dist/core/errors.d.ts.map +1 -1
- package/dist/core/errors.js +38 -0
- package/dist/core/errors.js.map +1 -1
- package/dist/core/types.d.ts +4 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/diff/comparator.d.ts +17 -0
- package/dist/diff/comparator.d.ts.map +1 -0
- package/dist/diff/comparator.js +158 -0
- package/dist/diff/comparator.js.map +1 -0
- package/dist/diff/engine.d.ts +10 -0
- package/dist/diff/engine.d.ts.map +1 -0
- package/dist/diff/engine.js +333 -0
- package/dist/diff/engine.js.map +1 -0
- package/dist/diff/hash.d.ts +6 -0
- package/dist/diff/hash.d.ts.map +1 -0
- package/dist/diff/hash.js +50 -0
- package/dist/diff/hash.js.map +1 -0
- package/dist/diff/key.d.ts +21 -0
- package/dist/diff/key.d.ts.map +1 -0
- package/dist/diff/key.js +107 -0
- package/dist/diff/key.js.map +1 -0
- package/dist/diff/reporter.d.ts +18 -0
- package/dist/diff/reporter.d.ts.map +1 -0
- package/dist/diff/reporter.js +133 -0
- package/dist/diff/reporter.js.map +1 -0
- package/dist/diff/schema.d.ts +7 -0
- package/dist/diff/schema.d.ts.map +1 -0
- package/dist/diff/schema.js +65 -0
- package/dist/diff/schema.js.map +1 -0
- package/dist/diff/storage/disk-index.d.ts +28 -0
- package/dist/diff/storage/disk-index.d.ts.map +1 -0
- package/dist/diff/storage/disk-index.js +105 -0
- package/dist/diff/storage/disk-index.js.map +1 -0
- package/dist/diff/storage/memory-index.d.ts +23 -0
- package/dist/diff/storage/memory-index.d.ts.map +1 -0
- package/dist/diff/storage/memory-index.js +69 -0
- package/dist/diff/storage/memory-index.js.map +1 -0
- package/dist/diff/storage/spillable-index.d.ts +30 -0
- package/dist/diff/storage/spillable-index.d.ts.map +1 -0
- package/dist/diff/storage/spillable-index.js +108 -0
- package/dist/diff/storage/spillable-index.js.map +1 -0
- package/dist/diff/types.d.ts +127 -0
- package/dist/diff/types.d.ts.map +1 -0
- package/dist/diff/types.js +2 -0
- package/dist/diff/types.js.map +1 -0
- package/dist/files/glob.d.ts +21 -0
- package/dist/files/glob.d.ts.map +1 -0
- package/dist/files/glob.js +132 -0
- package/dist/files/glob.js.map +1 -0
- package/dist/files/hash.d.ts +10 -0
- package/dist/files/hash.d.ts.map +1 -0
- package/dist/files/hash.js +67 -0
- package/dist/files/hash.js.map +1 -0
- package/dist/files/index.d.ts +6 -0
- package/dist/files/index.d.ts.map +1 -0
- package/dist/files/index.js +6 -0
- package/dist/files/index.js.map +1 -0
- package/dist/files/mime.d.ts +9 -0
- package/dist/files/mime.d.ts.map +1 -0
- package/dist/files/mime.js +128 -0
- package/dist/files/mime.js.map +1 -0
- package/dist/files/reader.d.ts +19 -0
- package/dist/files/reader.d.ts.map +1 -0
- package/dist/files/reader.js +344 -0
- package/dist/files/reader.js.map +1 -0
- package/dist/files/types.d.ts +47 -0
- package/dist/files/types.d.ts.map +1 -0
- package/dist/files/types.js +2 -0
- package/dist/files/types.js.map +1 -0
- package/dist/index.d.ts +17 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +19 -2
- package/dist/index.js.map +1 -1
- package/dist/readers/csv.d.ts.map +1 -1
- package/dist/readers/csv.js +59 -27
- package/dist/readers/csv.js.map +1 -1
- package/dist/readers/index.d.ts +2 -0
- package/dist/readers/index.d.ts.map +1 -1
- package/dist/readers/index.js +57 -3
- package/dist/readers/index.js.map +1 -1
- package/dist/readers/json.d.ts.map +1 -1
- package/dist/readers/json.js +6 -8
- package/dist/readers/json.js.map +1 -1
- package/dist/readers/jsonl.d.ts.map +1 -1
- package/dist/readers/jsonl.js +5 -7
- package/dist/readers/jsonl.js.map +1 -1
- package/dist/readers/parquet.d.ts +17 -0
- package/dist/readers/parquet.d.ts.map +1 -0
- package/dist/readers/parquet.js +146 -0
- package/dist/readers/parquet.js.map +1 -0
- package/dist/transforms/expression.d.ts.map +1 -1
- package/dist/transforms/expression.js +73 -7
- package/dist/transforms/expression.js.map +1 -1
- package/dist/utils/compression.d.ts +30 -6
- package/dist/utils/compression.d.ts.map +1 -1
- package/dist/utils/compression.js +95 -18
- package/dist/utils/compression.js.map +1 -1
- package/dist/writers/csv.d.ts.map +1 -1
- package/dist/writers/csv.js +29 -17
- package/dist/writers/csv.js.map +1 -1
- package/dist/writers/index.d.ts +2 -0
- package/dist/writers/index.d.ts.map +1 -1
- package/dist/writers/index.js +49 -4
- package/dist/writers/index.js.map +1 -1
- package/dist/writers/json.d.ts +1 -1
- package/dist/writers/json.d.ts.map +1 -1
- package/dist/writers/json.js +9 -11
- package/dist/writers/json.js.map +1 -1
- package/dist/writers/jsonl.d.ts +1 -1
- package/dist/writers/jsonl.d.ts.map +1 -1
- package/dist/writers/jsonl.js +9 -11
- package/dist/writers/jsonl.js.map +1 -1
- package/dist/writers/markdown.d.ts +21 -0
- package/dist/writers/markdown.d.ts.map +1 -0
- package/dist/writers/markdown.js +82 -0
- package/dist/writers/markdown.js.map +1 -0
- package/dist/writers/parquet.d.ts +19 -0
- package/dist/writers/parquet.d.ts.map +1 -0
- package/dist/writers/parquet.js +129 -0
- package/dist/writers/parquet.js.map +1 -0
- package/package.json +16 -3
- package/skills/rowpipe/SKILL.md +263 -0
- package/benchmarks/memory-bench.ts +0 -109
- package/src/analytics/reduce.ts +0 -388
- package/src/analytics/schema-inference.ts +0 -217
- package/src/analytics/semantic-types.ts +0 -36
- package/src/analytics/stats.ts +0 -383
- package/src/analytics/validator.ts +0 -134
- package/src/cli/commands/cast.ts +0 -60
- package/src/cli/commands/convert.ts +0 -128
- package/src/cli/commands/filter.ts +0 -58
- package/src/cli/commands/inspect.ts +0 -193
- package/src/cli/commands/map.ts +0 -66
- package/src/cli/commands/reduce.ts +0 -74
- package/src/cli/commands/rename.ts +0 -59
- package/src/cli/commands/sample.ts +0 -61
- package/src/cli/commands/schema.ts +0 -86
- package/src/cli/commands/select.ts +0 -59
- package/src/cli/commands/stats.ts +0 -100
- package/src/cli/commands/validate.ts +0 -123
- package/src/cli/index.ts +0 -285
- package/src/core/batch.ts +0 -81
- package/src/core/errors.ts +0 -81
- package/src/core/pipeline.ts +0 -150
- package/src/core/types.ts +0 -110
- package/src/index.ts +0 -52
- package/src/readers/csv.ts +0 -302
- package/src/readers/index.ts +0 -88
- package/src/readers/json.ts +0 -256
- package/src/readers/jsonl.ts +0 -126
- package/src/readers/xlsx.ts +0 -182
- package/src/transforms/cast.ts +0 -212
- package/src/transforms/expression.ts +0 -1114
- package/src/transforms/filter.ts +0 -38
- package/src/transforms/map.ts +0 -99
- package/src/transforms/rename.ts +0 -47
- package/src/transforms/sample.ts +0 -57
- package/src/transforms/select.ts +0 -64
- package/src/utils/compression.ts +0 -43
- package/src/utils/formatting.ts +0 -75
- package/src/utils/progress.ts +0 -37
- package/src/writers/csv.ts +0 -106
- package/src/writers/index.ts +0 -87
- package/src/writers/json.ts +0 -52
- package/src/writers/jsonl.ts +0 -49
- package/src/writers/xlsx.ts +0 -87
- package/tests/analytics.test.ts +0 -119
- package/tests/cli-integration.test.ts +0 -187
- package/tests/map-reduce.test.ts +0 -223
- package/tests/readers-writers.test.ts +0 -189
- package/tests/transforms.test.ts +0 -199
- package/tsconfig.json +0 -21
- package/vitest.config.ts +0 -9
package/src/analytics/stats.ts
DELETED
|
@@ -1,383 +0,0 @@
|
|
|
1
|
-
import type { Aggregator, Row } from "../core/types.js";
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Fast 32-bit FNV-1a hash function with 32-bit avalanche mixing.
|
|
5
|
-
* Zero BigInt allocations, runs at native CPU register speed.
|
|
6
|
-
*/
|
|
7
|
-
function hash32(str: string): number {
|
|
8
|
-
let h = 0x811c9dc5;
|
|
9
|
-
const len = str.length;
|
|
10
|
-
for (let i = 0; i < len; i++) {
|
|
11
|
-
h = (h ^ str.charCodeAt(i)) >>> 0;
|
|
12
|
-
h = Math.imul(h, 0x01000193) >>> 0;
|
|
13
|
-
}
|
|
14
|
-
// Avalanche mixing
|
|
15
|
-
h ^= h >>> 16;
|
|
16
|
-
h = Math.imul(h, 0x85ebca6b) >>> 0;
|
|
17
|
-
h ^= h >>> 13;
|
|
18
|
-
h = Math.imul(h, 0xc2b2ae35) >>> 0;
|
|
19
|
-
h ^= h >>> 16;
|
|
20
|
-
return h >>> 0;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* HyperLogLog (HLL) distinct count estimator with bounded O(1) memory.
|
|
25
|
-
* Uses m = 1024 registers (p = 10), standard error ~3.25%.
|
|
26
|
-
* Hardware Math.clz32 for instantaneous zero counting.
|
|
27
|
-
*/
|
|
28
|
-
export class HyperLogLog {
|
|
29
|
-
private p: number;
|
|
30
|
-
private m: number;
|
|
31
|
-
private registers: Uint8Array;
|
|
32
|
-
private alphaMM: number;
|
|
33
|
-
|
|
34
|
-
constructor(p = 10) {
|
|
35
|
-
this.p = p;
|
|
36
|
-
this.m = 1 << p;
|
|
37
|
-
this.registers = new Uint8Array(this.m);
|
|
38
|
-
|
|
39
|
-
// Alpha constant calculation
|
|
40
|
-
if (this.m === 16) this.alphaMM = 0.673 * this.m * this.m;
|
|
41
|
-
else if (this.m === 32) this.alphaMM = 0.697 * this.m * this.m;
|
|
42
|
-
else if (this.m === 64) this.alphaMM = 0.709 * this.m * this.m;
|
|
43
|
-
else this.alphaMM = (0.7213 / (1 + 1.079 / this.m)) * this.m * this.m;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
add(value: unknown): void {
|
|
47
|
-
if (value === null || value === undefined) return;
|
|
48
|
-
const str = String(value);
|
|
49
|
-
const hash = hash32(str);
|
|
50
|
-
|
|
51
|
-
// Low p bits for register index
|
|
52
|
-
const index = hash & (this.m - 1);
|
|
53
|
-
// Remaining (32 - p) bits for leading zeros
|
|
54
|
-
const w = hash >>> this.p;
|
|
55
|
-
|
|
56
|
-
// Single-cycle CPU instruction Math.clz32 for leading zero count
|
|
57
|
-
let leadingZeros: number;
|
|
58
|
-
if (w === 0) {
|
|
59
|
-
leadingZeros = 32 - this.p + 1;
|
|
60
|
-
} else {
|
|
61
|
-
leadingZeros = Math.clz32(w) - this.p + 1;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
if (leadingZeros > this.registers[index]!) {
|
|
65
|
-
this.registers[index] = leadingZeros;
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
merge(other: HyperLogLog): void {
|
|
70
|
-
for (let i = 0; i < this.m; i++) {
|
|
71
|
-
if (other.registers[i]! > this.registers[i]!) {
|
|
72
|
-
this.registers[i] = other.registers[i]!;
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
count(): number {
|
|
78
|
-
let sum = 0;
|
|
79
|
-
let zeros = 0;
|
|
80
|
-
|
|
81
|
-
for (let i = 0; i < this.m; i++) {
|
|
82
|
-
const val = this.registers[i]!;
|
|
83
|
-
sum += 2 ** -val;
|
|
84
|
-
if (val === 0) zeros++;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
let estimate = this.alphaMM / sum;
|
|
88
|
-
|
|
89
|
-
// Small range correction (Linear Counting)
|
|
90
|
-
if (estimate <= 2.5 * this.m && zeros > 0) {
|
|
91
|
-
estimate = this.m * Math.log(this.m / zeros);
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
return Math.round(estimate);
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
export interface NumericColumnStats {
|
|
99
|
-
count: number;
|
|
100
|
-
nullCount: number;
|
|
101
|
-
min: number | null;
|
|
102
|
-
max: number | null;
|
|
103
|
-
sum: number;
|
|
104
|
-
mean: number | null;
|
|
105
|
-
variance: number | null;
|
|
106
|
-
stddev: number | null;
|
|
107
|
-
approxDistinct: number;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
export interface StringColumnStats {
|
|
111
|
-
count: number;
|
|
112
|
-
nullCount: number;
|
|
113
|
-
emptyCount: number;
|
|
114
|
-
minLength: number | null;
|
|
115
|
-
maxLength: number | null;
|
|
116
|
-
avgLength: number | null;
|
|
117
|
-
emptyCountRatio?: number;
|
|
118
|
-
approxDistinct: number;
|
|
119
|
-
topValues: Array<{ value: string; count: number }>;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
export interface ColumnStatsResult {
|
|
123
|
-
column: string;
|
|
124
|
-
type: "numeric" | "string";
|
|
125
|
-
numeric?: NumericColumnStats;
|
|
126
|
-
string?: StringColumnStats;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
export interface DatasetStatsResult {
|
|
130
|
-
totalRows: number;
|
|
131
|
-
columns: Record<string, ColumnStatsResult>;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
/**
|
|
135
|
-
* Online Welford statistics collector for a numeric column.
|
|
136
|
-
*/
|
|
137
|
-
export class NumericStatsCollector {
|
|
138
|
-
public count = 0;
|
|
139
|
-
public nullCount = 0;
|
|
140
|
-
public min: number | null = null;
|
|
141
|
-
public max: number | null = null;
|
|
142
|
-
public sum = 0;
|
|
143
|
-
public mean = 0;
|
|
144
|
-
public M2 = 0; // sum of squared differences from the mean
|
|
145
|
-
public hll = new HyperLogLog();
|
|
146
|
-
|
|
147
|
-
add(val: unknown): void {
|
|
148
|
-
if (val === null || val === undefined || val === "") {
|
|
149
|
-
this.nullCount++;
|
|
150
|
-
return;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
const num = typeof val === "number" ? val : Number(val);
|
|
154
|
-
if (Number.isNaN(num)) {
|
|
155
|
-
this.nullCount++;
|
|
156
|
-
return;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
this.count++;
|
|
160
|
-
this.sum += num;
|
|
161
|
-
this.hll.add(num);
|
|
162
|
-
|
|
163
|
-
if (this.min === null || num < this.min) this.min = num;
|
|
164
|
-
if (this.max === null || num > this.max) this.max = num;
|
|
165
|
-
|
|
166
|
-
// Welford's online algorithm
|
|
167
|
-
const delta = num - this.mean;
|
|
168
|
-
this.mean += delta / this.count;
|
|
169
|
-
const delta2 = num - this.mean;
|
|
170
|
-
this.M2 += delta * delta2;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
merge(other: NumericStatsCollector): void {
|
|
174
|
-
if (other.count === 0) {
|
|
175
|
-
this.nullCount += other.nullCount;
|
|
176
|
-
return;
|
|
177
|
-
}
|
|
178
|
-
if (this.count === 0) {
|
|
179
|
-
this.count = other.count;
|
|
180
|
-
this.nullCount = other.nullCount;
|
|
181
|
-
this.min = other.min;
|
|
182
|
-
this.max = other.max;
|
|
183
|
-
this.sum = other.sum;
|
|
184
|
-
this.mean = other.mean;
|
|
185
|
-
this.M2 = other.M2;
|
|
186
|
-
this.hll.merge(other.hll);
|
|
187
|
-
return;
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
const totalCount = this.count + other.count;
|
|
191
|
-
const delta = other.mean - this.mean;
|
|
192
|
-
|
|
193
|
-
this.mean = (this.count * this.mean + other.count * other.mean) / totalCount;
|
|
194
|
-
this.M2 = this.M2 + other.M2 + (delta * delta * this.count * other.count) / totalCount;
|
|
195
|
-
this.count = totalCount;
|
|
196
|
-
this.nullCount += other.nullCount;
|
|
197
|
-
this.sum += other.sum;
|
|
198
|
-
|
|
199
|
-
if (this.min === null || (other.min !== null && other.min < this.min)) this.min = other.min;
|
|
200
|
-
if (this.max === null || (other.max !== null && other.max > this.max)) this.max = other.max;
|
|
201
|
-
this.hll.merge(other.hll);
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
result(): NumericColumnStats {
|
|
205
|
-
const variance = this.count > 1 ? this.M2 / (this.count - 1) : this.count === 1 ? 0 : null;
|
|
206
|
-
const stddev = variance !== null ? Math.sqrt(variance) : null;
|
|
207
|
-
|
|
208
|
-
return {
|
|
209
|
-
count: this.count,
|
|
210
|
-
nullCount: this.nullCount,
|
|
211
|
-
min: this.min,
|
|
212
|
-
max: this.max,
|
|
213
|
-
sum: this.sum,
|
|
214
|
-
mean: this.count > 0 ? this.mean : null,
|
|
215
|
-
variance,
|
|
216
|
-
stddev,
|
|
217
|
-
approxDistinct: this.hll.count(),
|
|
218
|
-
};
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
/**
|
|
223
|
-
* Online statistics collector for a string column.
|
|
224
|
-
*/
|
|
225
|
-
export class StringStatsCollector {
|
|
226
|
-
public count = 0;
|
|
227
|
-
public nullCount = 0;
|
|
228
|
-
public emptyCount = 0;
|
|
229
|
-
public minLength: number | null = null;
|
|
230
|
-
public maxLength: number | null = null;
|
|
231
|
-
public totalLength = 0;
|
|
232
|
-
public hll = new HyperLogLog();
|
|
233
|
-
private frequencyMap = new Map<string, number>();
|
|
234
|
-
private maxTrackedValues = 1000;
|
|
235
|
-
|
|
236
|
-
add(val: unknown): void {
|
|
237
|
-
if (val === null || val === undefined) {
|
|
238
|
-
this.nullCount++;
|
|
239
|
-
return;
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
const str = String(val);
|
|
243
|
-
if (str === "") {
|
|
244
|
-
this.emptyCount++;
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
this.count++;
|
|
248
|
-
this.totalLength += str.length;
|
|
249
|
-
this.hll.add(str);
|
|
250
|
-
|
|
251
|
-
if (this.minLength === null || str.length < this.minLength) this.minLength = str.length;
|
|
252
|
-
if (this.maxLength === null || str.length > this.maxLength) this.maxLength = str.length;
|
|
253
|
-
|
|
254
|
-
// Track top values bounded
|
|
255
|
-
if (this.frequencyMap.size < this.maxTrackedValues || this.frequencyMap.has(str)) {
|
|
256
|
-
this.frequencyMap.set(str, (this.frequencyMap.get(str) || 0) + 1);
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
result(): StringColumnStats {
|
|
261
|
-
const sorted = Array.from(this.frequencyMap.entries())
|
|
262
|
-
.sort((a, b) => b[1] - a[1])
|
|
263
|
-
.slice(0, 5)
|
|
264
|
-
.map(([value, count]) => ({ value, count }));
|
|
265
|
-
|
|
266
|
-
return {
|
|
267
|
-
count: this.count,
|
|
268
|
-
nullCount: this.nullCount,
|
|
269
|
-
emptyCount: this.emptyCount,
|
|
270
|
-
minLength: this.minLength,
|
|
271
|
-
maxLength: this.maxLength,
|
|
272
|
-
avgLength: this.count > 0 ? this.totalLength / this.count : null,
|
|
273
|
-
approxDistinct: this.hll.count(),
|
|
274
|
-
topValues: sorted,
|
|
275
|
-
};
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
interface ColumnHandler {
|
|
280
|
-
col: string;
|
|
281
|
-
numCollector: NumericStatsCollector;
|
|
282
|
-
strCollector: StringStatsCollector;
|
|
283
|
-
hint: { numericVotes: number; totalVotes: number };
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
/**
|
|
287
|
-
* Dataset-wide Aggregator collecting statistics across all columns in stream.
|
|
288
|
-
*/
|
|
289
|
-
export class DatasetStatsAggregator implements Aggregator<DatasetStatsResult> {
|
|
290
|
-
private totalRows = 0;
|
|
291
|
-
private targetColumn?: string;
|
|
292
|
-
private numericCollectors = new Map<string, NumericStatsCollector>();
|
|
293
|
-
private stringCollectors = new Map<string, StringStatsCollector>();
|
|
294
|
-
private columnTypeHints = new Map<string, { numericVotes: number; totalVotes: number }>();
|
|
295
|
-
private handlers: ColumnHandler[] = [];
|
|
296
|
-
private initialized = false;
|
|
297
|
-
|
|
298
|
-
constructor(options?: { column?: string }) {
|
|
299
|
-
this.targetColumn = options?.column;
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
private initHandler(col: string): ColumnHandler {
|
|
303
|
-
let numCollector = this.numericCollectors.get(col);
|
|
304
|
-
let strCollector = this.stringCollectors.get(col);
|
|
305
|
-
let hint = this.columnTypeHints.get(col);
|
|
306
|
-
|
|
307
|
-
if (!numCollector) {
|
|
308
|
-
numCollector = new NumericStatsCollector();
|
|
309
|
-
strCollector = new StringStatsCollector();
|
|
310
|
-
hint = { numericVotes: 0, totalVotes: 0 };
|
|
311
|
-
this.numericCollectors.set(col, numCollector);
|
|
312
|
-
this.stringCollectors.set(col, strCollector);
|
|
313
|
-
this.columnTypeHints.set(col, hint);
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
return { col, numCollector: numCollector!, strCollector: strCollector!, hint: hint! };
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
add(row: Row): void {
|
|
320
|
-
this.totalRows++;
|
|
321
|
-
|
|
322
|
-
if (!this.initialized) {
|
|
323
|
-
const columns = this.targetColumn ? [this.targetColumn] : Object.keys(row);
|
|
324
|
-
this.handlers = new Array(columns.length);
|
|
325
|
-
for (let i = 0; i < columns.length; i++) {
|
|
326
|
-
this.handlers[i] = this.initHandler(columns[i]!);
|
|
327
|
-
}
|
|
328
|
-
this.initialized = true;
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
const handlers = this.handlers;
|
|
332
|
-
const len = handlers.length;
|
|
333
|
-
|
|
334
|
-
for (let i = 0; i < len; i++) {
|
|
335
|
-
const h = handlers[i]!;
|
|
336
|
-
const val = row[h.col];
|
|
337
|
-
|
|
338
|
-
h.strCollector.add(val);
|
|
339
|
-
|
|
340
|
-
if (val !== null && val !== undefined && val !== "") {
|
|
341
|
-
h.hint.totalVotes++;
|
|
342
|
-
const num = typeof val === "number" ? val : Number(val);
|
|
343
|
-
if (!Number.isNaN(num) && typeof val !== "boolean") {
|
|
344
|
-
h.hint.numericVotes++;
|
|
345
|
-
h.numCollector.add(num);
|
|
346
|
-
} else {
|
|
347
|
-
h.numCollector.add(null);
|
|
348
|
-
}
|
|
349
|
-
} else {
|
|
350
|
-
h.numCollector.add(null);
|
|
351
|
-
}
|
|
352
|
-
}
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
result(): DatasetStatsResult {
|
|
356
|
-
const columns: Record<string, ColumnStatsResult> = {};
|
|
357
|
-
|
|
358
|
-
for (const [col, hint] of this.columnTypeHints.entries()) {
|
|
359
|
-
const isNumeric = hint.totalVotes > 0 && hint.numericVotes / hint.totalVotes >= 0.8;
|
|
360
|
-
const numCollector = this.numericCollectors.get(col)!;
|
|
361
|
-
const strCollector = this.stringCollectors.get(col)!;
|
|
362
|
-
|
|
363
|
-
if (isNumeric) {
|
|
364
|
-
columns[col] = {
|
|
365
|
-
column: col,
|
|
366
|
-
type: "numeric",
|
|
367
|
-
numeric: numCollector.result(),
|
|
368
|
-
};
|
|
369
|
-
} else {
|
|
370
|
-
columns[col] = {
|
|
371
|
-
column: col,
|
|
372
|
-
type: "string",
|
|
373
|
-
string: strCollector.result(),
|
|
374
|
-
};
|
|
375
|
-
}
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
return {
|
|
379
|
-
totalRows: this.totalRows,
|
|
380
|
-
columns,
|
|
381
|
-
};
|
|
382
|
-
}
|
|
383
|
-
}
|
|
@@ -1,134 +0,0 @@
|
|
|
1
|
-
import type { Aggregator, Row } from "../core/types.js";
|
|
2
|
-
import { classifyPrimitiveType } from "./schema-inference.js";
|
|
3
|
-
import { detectSemanticType } from "./semantic-types.js";
|
|
4
|
-
|
|
5
|
-
export interface ColumnSchemaRule {
|
|
6
|
-
type?: string;
|
|
7
|
-
nullable?: boolean;
|
|
8
|
-
format?: string;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export type ValidationSchemaDefinition = Record<string, ColumnSchemaRule>;
|
|
12
|
-
|
|
13
|
-
export interface ColumnViolationSummary {
|
|
14
|
-
column: string;
|
|
15
|
-
rule: string;
|
|
16
|
-
violationsCount: number;
|
|
17
|
-
sampleInvalidValues: unknown[];
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export interface ValidationReport {
|
|
21
|
-
totalRows: number;
|
|
22
|
-
validRows: number;
|
|
23
|
-
invalidRows: number;
|
|
24
|
-
isValid: boolean;
|
|
25
|
-
violations: ColumnViolationSummary[];
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export class SchemaValidatorAggregator implements Aggregator<ValidationReport> {
|
|
29
|
-
private schema: ValidationSchemaDefinition;
|
|
30
|
-
private totalRows = 0;
|
|
31
|
-
private validRows = 0;
|
|
32
|
-
private invalidRows = 0;
|
|
33
|
-
private violationsMap = new Map<string, ColumnViolationSummary>();
|
|
34
|
-
|
|
35
|
-
constructor(schema: ValidationSchemaDefinition) {
|
|
36
|
-
this.schema = schema;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
private addViolation(column: string, rule: string, value: unknown): void {
|
|
40
|
-
const key = `${column}::${rule}`;
|
|
41
|
-
if (!this.violationsMap.has(key)) {
|
|
42
|
-
this.violationsMap.set(key, {
|
|
43
|
-
column,
|
|
44
|
-
rule,
|
|
45
|
-
violationsCount: 0,
|
|
46
|
-
sampleInvalidValues: [],
|
|
47
|
-
});
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
const v = this.violationsMap.get(key)!;
|
|
51
|
-
v.violationsCount++;
|
|
52
|
-
if (v.sampleInvalidValues.length < 5) {
|
|
53
|
-
v.sampleInvalidValues.push(value);
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
add(row: Row): void {
|
|
58
|
-
this.totalRows++;
|
|
59
|
-
let rowHasError = false;
|
|
60
|
-
|
|
61
|
-
for (const [colName, rule] of Object.entries(this.schema)) {
|
|
62
|
-
const val = row[colName];
|
|
63
|
-
const isNull = val === null || val === undefined || val === "";
|
|
64
|
-
|
|
65
|
-
// Nullability check
|
|
66
|
-
if (isNull) {
|
|
67
|
-
if (rule.nullable === false) {
|
|
68
|
-
this.addViolation(colName, "null not allowed", val);
|
|
69
|
-
rowHasError = true;
|
|
70
|
-
}
|
|
71
|
-
continue;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
// Type check
|
|
75
|
-
if (rule.type) {
|
|
76
|
-
const actualType = classifyPrimitiveType(val);
|
|
77
|
-
const expectedType = rule.type.toLowerCase();
|
|
78
|
-
|
|
79
|
-
let matchesType = false;
|
|
80
|
-
if (expectedType === "string") {
|
|
81
|
-
matchesType = true;
|
|
82
|
-
} else if (expectedType === "integer" || expectedType === "int") {
|
|
83
|
-
matchesType = actualType === "integer";
|
|
84
|
-
} else if (expectedType === "number" || expectedType === "float") {
|
|
85
|
-
matchesType = actualType === "integer" || actualType === "number";
|
|
86
|
-
} else if (expectedType === "boolean" || expectedType === "bool") {
|
|
87
|
-
matchesType = actualType === "boolean";
|
|
88
|
-
} else if (expectedType === "date") {
|
|
89
|
-
matchesType = actualType === "date" || actualType === "datetime";
|
|
90
|
-
} else if (expectedType === "datetime") {
|
|
91
|
-
matchesType = actualType === "datetime";
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
if (!matchesType) {
|
|
95
|
-
this.addViolation(
|
|
96
|
-
colName,
|
|
97
|
-
`expected ${rule.type}`,
|
|
98
|
-
val
|
|
99
|
-
);
|
|
100
|
-
rowHasError = true;
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
// Format check (semantic type)
|
|
105
|
-
if (rule.format) {
|
|
106
|
-
const detected = detectSemanticType(val);
|
|
107
|
-
if (detected !== rule.format.toLowerCase()) {
|
|
108
|
-
this.addViolation(
|
|
109
|
-
colName,
|
|
110
|
-
`expected format ${rule.format}`,
|
|
111
|
-
val
|
|
112
|
-
);
|
|
113
|
-
rowHasError = true;
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
if (rowHasError) {
|
|
119
|
-
this.invalidRows++;
|
|
120
|
-
} else {
|
|
121
|
-
this.validRows++;
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
result(): ValidationReport {
|
|
126
|
-
return {
|
|
127
|
-
totalRows: this.totalRows,
|
|
128
|
-
validRows: this.validRows,
|
|
129
|
-
invalidRows: this.invalidRows,
|
|
130
|
-
isValid: this.invalidRows === 0,
|
|
131
|
-
violations: Array.from(this.violationsMap.values()),
|
|
132
|
-
};
|
|
133
|
-
}
|
|
134
|
-
}
|
package/src/cli/commands/cast.ts
DELETED
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
import { createPipeline } from "../../core/pipeline.js";
|
|
2
|
-
import { createReader, inferFormatFromPath as inferReaderFormat } from "../../readers/index.js";
|
|
3
|
-
import { createWriter } from "../../writers/index.js";
|
|
4
|
-
import { castColumns, type CastOnError, parseCastSpecs } from "../../transforms/cast.js";
|
|
5
|
-
import { openReadableStream } from "../../utils/compression.js";
|
|
6
|
-
import { logMemoryDebug } from "../../utils/formatting.js";
|
|
7
|
-
import { ProgressReporter } from "../../utils/progress.js";
|
|
8
|
-
|
|
9
|
-
export interface CastCommandOptions {
|
|
10
|
-
onError?: CastOnError;
|
|
11
|
-
from?: string;
|
|
12
|
-
to?: string;
|
|
13
|
-
sheet?: string;
|
|
14
|
-
delimiter?: string;
|
|
15
|
-
batchSize?: string | number;
|
|
16
|
-
quiet?: boolean;
|
|
17
|
-
noProgress?: boolean;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export async function castCommand(
|
|
21
|
-
inputPath = "-",
|
|
22
|
-
specs: string[],
|
|
23
|
-
options: CastCommandOptions = {}
|
|
24
|
-
): Promise<void> {
|
|
25
|
-
const effectiveBatchSize = Number(options.batchSize) || 1000;
|
|
26
|
-
const progress = new ProgressReporter(options);
|
|
27
|
-
|
|
28
|
-
let fromFormat = options.from?.toLowerCase();
|
|
29
|
-
if (!fromFormat && inputPath !== "-") {
|
|
30
|
-
fromFormat = inferReaderFormat(inputPath) ?? undefined;
|
|
31
|
-
}
|
|
32
|
-
if (!fromFormat) {
|
|
33
|
-
fromFormat = "csv";
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
const toFormat = options.to?.toLowerCase() || (fromFormat === "xlsx" ? "csv" : fromFormat);
|
|
37
|
-
const mapping = parseCastSpecs(specs);
|
|
38
|
-
|
|
39
|
-
const inputStream = openReadableStream(inputPath);
|
|
40
|
-
const reader = createReader(inputStream, {
|
|
41
|
-
format: fromFormat,
|
|
42
|
-
sheet: options.sheet,
|
|
43
|
-
delimiter: options.delimiter,
|
|
44
|
-
batchSize: effectiveBatchSize,
|
|
45
|
-
filePath: inputPath,
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
const writer = createWriter(process.stdout, {
|
|
49
|
-
format: toFormat,
|
|
50
|
-
delimiter: options.delimiter,
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
const pipeline = createPipeline(reader, { batchSize: effectiveBatchSize });
|
|
54
|
-
pipeline.pipe(castColumns(mapping, { onError: options.onError }));
|
|
55
|
-
pipeline.onProgress((info) => progress.update(info));
|
|
56
|
-
|
|
57
|
-
await pipeline.to(writer);
|
|
58
|
-
progress.done();
|
|
59
|
-
logMemoryDebug();
|
|
60
|
-
}
|
|
@@ -1,128 +0,0 @@
|
|
|
1
|
-
import { mkdir } from "node:fs/promises";
|
|
2
|
-
import { join } from "node:path";
|
|
3
|
-
import { createPipeline } from "../../core/pipeline.js";
|
|
4
|
-
import { createReader, inferFormatFromPath as inferReaderFormat } from "../../readers/index.js";
|
|
5
|
-
import { XLSXReader } from "../../readers/xlsx.js";
|
|
6
|
-
import { createWriter, inferFormatFromPath as inferWriterFormat } from "../../writers/index.js";
|
|
7
|
-
import { openReadableStream, openWritableStream } from "../../utils/compression.js";
|
|
8
|
-
import { formatNumber, logMemoryDebug } from "../../utils/formatting.js";
|
|
9
|
-
import { ProgressReporter } from "../../utils/progress.js";
|
|
10
|
-
|
|
11
|
-
export interface ConvertCommandOptions {
|
|
12
|
-
from?: string;
|
|
13
|
-
to?: string;
|
|
14
|
-
sheet?: string;
|
|
15
|
-
allSheets?: boolean;
|
|
16
|
-
outDir?: string;
|
|
17
|
-
path?: string;
|
|
18
|
-
delimiter?: string;
|
|
19
|
-
header?: boolean;
|
|
20
|
-
batchSize?: string | number;
|
|
21
|
-
quiet?: boolean;
|
|
22
|
-
noProgress?: boolean;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export async function convertCommand(
|
|
26
|
-
inputPath = "-",
|
|
27
|
-
outputPath?: string,
|
|
28
|
-
options: ConvertCommandOptions = {}
|
|
29
|
-
): Promise<void> {
|
|
30
|
-
const effectiveBatchSize = Number(options.batchSize) || 1000;
|
|
31
|
-
|
|
32
|
-
// Determine input format
|
|
33
|
-
let fromFormat = options.from?.toLowerCase();
|
|
34
|
-
if (!fromFormat && inputPath !== "-") {
|
|
35
|
-
fromFormat = inferReaderFormat(inputPath) ?? undefined;
|
|
36
|
-
}
|
|
37
|
-
if (!fromFormat) {
|
|
38
|
-
fromFormat = "csv";
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
// Handle multi-sheet export if --all-sheets is specified
|
|
42
|
-
if (options.allSheets && fromFormat === "xlsx" && inputPath !== "-") {
|
|
43
|
-
const outDir = options.outDir || outputPath || "./";
|
|
44
|
-
const toFormat = (options.to?.toLowerCase() || "csv").replace(/^\./, "");
|
|
45
|
-
|
|
46
|
-
await mkdir(outDir, { recursive: true });
|
|
47
|
-
|
|
48
|
-
const xlsxReader = new XLSXReader(inputPath);
|
|
49
|
-
const sheets = await xlsxReader.getParsedSheets();
|
|
50
|
-
|
|
51
|
-
if (sheets.length === 0) {
|
|
52
|
-
process.stderr.write("No sheets found in workbook.\n");
|
|
53
|
-
return;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
process.stderr.write(`Exporting ${sheets.length} sheets to "${outDir}"...\n`);
|
|
57
|
-
|
|
58
|
-
for (const sheet of sheets) {
|
|
59
|
-
const sheetName = sheet.sheet;
|
|
60
|
-
const targetFileName = `${sheetName}.${toFormat}`;
|
|
61
|
-
const targetFilePath = join(outDir, targetFileName);
|
|
62
|
-
|
|
63
|
-
const sheetReader = createReader(inputPath, {
|
|
64
|
-
format: "xlsx",
|
|
65
|
-
sheet: sheetName,
|
|
66
|
-
batchSize: effectiveBatchSize,
|
|
67
|
-
filePath: inputPath,
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
const sheetWriter = createWriter(targetFilePath, {
|
|
71
|
-
format: toFormat,
|
|
72
|
-
delimiter: options.delimiter,
|
|
73
|
-
header: options.header,
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
const pipeline = createPipeline(sheetReader, { batchSize: effectiveBatchSize });
|
|
77
|
-
await pipeline.to(sheetWriter);
|
|
78
|
-
|
|
79
|
-
const rowCount = Math.max(0, sheet.data.length - 1);
|
|
80
|
-
process.stderr.write(
|
|
81
|
-
` ✓ Sheet "${sheetName}" -> ${targetFilePath} (${formatNumber(rowCount)} rows)\n`
|
|
82
|
-
);
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
process.stderr.write("Done.\n");
|
|
86
|
-
logMemoryDebug();
|
|
87
|
-
return;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
// Determine output format
|
|
91
|
-
let toFormat = options.to?.toLowerCase();
|
|
92
|
-
if (!toFormat && outputPath && outputPath !== "-") {
|
|
93
|
-
toFormat = inferWriterFormat(outputPath) ?? undefined;
|
|
94
|
-
}
|
|
95
|
-
if (!toFormat) {
|
|
96
|
-
toFormat = "csv";
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
const progress = new ProgressReporter(options);
|
|
100
|
-
const effectiveOutput = outputPath || "-";
|
|
101
|
-
|
|
102
|
-
const inputStream = openReadableStream(inputPath);
|
|
103
|
-
const outputStream = openWritableStream(effectiveOutput);
|
|
104
|
-
|
|
105
|
-
const reader = createReader(inputStream, {
|
|
106
|
-
format: fromFormat,
|
|
107
|
-
sheet: options.sheet,
|
|
108
|
-
path: options.path,
|
|
109
|
-
delimiter: options.delimiter,
|
|
110
|
-
header: options.header,
|
|
111
|
-
batchSize: effectiveBatchSize,
|
|
112
|
-
filePath: inputPath,
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
const writer = createWriter(outputStream, {
|
|
116
|
-
format: toFormat,
|
|
117
|
-
delimiter: options.delimiter,
|
|
118
|
-
header: options.header,
|
|
119
|
-
sheet: options.sheet,
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
const pipeline = createPipeline(reader, { batchSize: effectiveBatchSize });
|
|
123
|
-
pipeline.onProgress((info) => progress.update(info));
|
|
124
|
-
|
|
125
|
-
await pipeline.to(writer);
|
|
126
|
-
progress.done();
|
|
127
|
-
logMemoryDebug();
|
|
128
|
-
}
|