rowpipe 1.0.0 → 1.1.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 +25 -32
- package/dist/cli/index.js +4 -4
- package/dist/cli/index.js.map +1 -1
- package/dist/core/types.d.ts +2 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/readers/csv.d.ts.map +1 -1
- package/dist/readers/csv.js +28 -16
- package/dist/readers/csv.js.map +1 -1
- package/dist/readers/index.d.ts +1 -0
- package/dist/readers/index.d.ts.map +1 -1
- package/dist/readers/index.js +29 -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/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 +9 -11
- 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 +13 -2
- package/skills/rowpipe/SKILL.md +181 -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
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
import type { SemanticType } from "../core/types.js";
|
|
2
|
-
|
|
3
|
-
const EMAIL_REGEX = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
|
|
4
|
-
const URL_REGEX = /^https?:\/\/[^\s/$.?#].[^\s]*$/i;
|
|
5
|
-
const UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
6
|
-
const IPV4_REGEX = /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/;
|
|
7
|
-
const IPV6_REGEX = /^(?:[A-F0-9]{1,4}:){7}[A-F0-9]{1,4}$/i;
|
|
8
|
-
const PHONE_REGEX = /^\+?[0-9\s\-().]{7,20}$/;
|
|
9
|
-
|
|
10
|
-
const ISO_COUNTRY_CODES = new Set([
|
|
11
|
-
"US", "GB", "DE", "FR", "TR", "CA", "AU", "JP", "CN", "IN", "BR", "IT", "ES", "NL", "SE", "CH", "PL", "MX", "KR", "RU", "ZA", "EG", "SA", "AE", "SG", "NZ", "IE", "NO", "DK", "FI", "PT", "GR", "AT", "BE", "IL", "AR", "CL", "CO", "PE", "TH", "MY", "ID", "PH", "VN", "PK", "NG", "KE", "ZA"
|
|
12
|
-
]);
|
|
13
|
-
|
|
14
|
-
const ISO_CURRENCIES = new Set([
|
|
15
|
-
"USD", "EUR", "GBP", "TRY", "JPY", "CAD", "AUD", "CHF", "CNY", "INR", "BRL", "RUB", "KRW", "SEK", "NOK", "MXN", "SGD", "NZD", "HKD", "PLN", "ZAR", "AED", "SAR", "THB", "IDR"
|
|
16
|
-
]);
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* Tests if a string value matches a specific semantic pattern.
|
|
20
|
-
*/
|
|
21
|
-
export function detectSemanticType(value: unknown): SemanticType | null {
|
|
22
|
-
if (value === null || value === undefined) return null;
|
|
23
|
-
const str = String(value).trim();
|
|
24
|
-
if (str.length === 0) return null;
|
|
25
|
-
|
|
26
|
-
if (UUID_REGEX.test(str)) return "uuid";
|
|
27
|
-
if (EMAIL_REGEX.test(str)) return "email";
|
|
28
|
-
if (URL_REGEX.test(str)) return "url";
|
|
29
|
-
if (IPV4_REGEX.test(str)) return "ipv4";
|
|
30
|
-
if (IPV6_REGEX.test(str)) return "ipv6";
|
|
31
|
-
if (str.length >= 2 && str.length <= 3 && ISO_COUNTRY_CODES.has(str.toUpperCase())) return "country-code";
|
|
32
|
-
if (str.length === 3 && ISO_CURRENCIES.has(str.toUpperCase())) return "currency";
|
|
33
|
-
if (PHONE_REGEX.test(str) && /\d{4,}/.test(str) && (str.startsWith("+") || str.includes("-") || str.includes("("))) return "phone";
|
|
34
|
-
|
|
35
|
-
return null;
|
|
36
|
-
}
|
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
|
-
}
|