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.
Files changed (106) hide show
  1. package/README.md +25 -32
  2. package/dist/cli/index.js +4 -4
  3. package/dist/cli/index.js.map +1 -1
  4. package/dist/core/types.d.ts +2 -0
  5. package/dist/core/types.d.ts.map +1 -1
  6. package/dist/index.d.ts +2 -2
  7. package/dist/index.d.ts.map +1 -1
  8. package/dist/index.js +2 -2
  9. package/dist/index.js.map +1 -1
  10. package/dist/readers/csv.d.ts.map +1 -1
  11. package/dist/readers/csv.js +28 -16
  12. package/dist/readers/csv.js.map +1 -1
  13. package/dist/readers/index.d.ts +1 -0
  14. package/dist/readers/index.d.ts.map +1 -1
  15. package/dist/readers/index.js +29 -3
  16. package/dist/readers/index.js.map +1 -1
  17. package/dist/readers/json.d.ts.map +1 -1
  18. package/dist/readers/json.js +6 -8
  19. package/dist/readers/json.js.map +1 -1
  20. package/dist/readers/jsonl.d.ts.map +1 -1
  21. package/dist/readers/jsonl.js +5 -7
  22. package/dist/readers/jsonl.js.map +1 -1
  23. package/dist/readers/parquet.d.ts +17 -0
  24. package/dist/readers/parquet.d.ts.map +1 -0
  25. package/dist/readers/parquet.js +146 -0
  26. package/dist/readers/parquet.js.map +1 -0
  27. package/dist/utils/compression.d.ts +30 -6
  28. package/dist/utils/compression.d.ts.map +1 -1
  29. package/dist/utils/compression.js +95 -18
  30. package/dist/utils/compression.js.map +1 -1
  31. package/dist/writers/csv.d.ts.map +1 -1
  32. package/dist/writers/csv.js +9 -11
  33. package/dist/writers/csv.js.map +1 -1
  34. package/dist/writers/index.d.ts +2 -0
  35. package/dist/writers/index.d.ts.map +1 -1
  36. package/dist/writers/index.js +49 -4
  37. package/dist/writers/index.js.map +1 -1
  38. package/dist/writers/json.d.ts +1 -1
  39. package/dist/writers/json.d.ts.map +1 -1
  40. package/dist/writers/json.js +9 -11
  41. package/dist/writers/json.js.map +1 -1
  42. package/dist/writers/jsonl.d.ts +1 -1
  43. package/dist/writers/jsonl.d.ts.map +1 -1
  44. package/dist/writers/jsonl.js +9 -11
  45. package/dist/writers/jsonl.js.map +1 -1
  46. package/dist/writers/markdown.d.ts +21 -0
  47. package/dist/writers/markdown.d.ts.map +1 -0
  48. package/dist/writers/markdown.js +82 -0
  49. package/dist/writers/markdown.js.map +1 -0
  50. package/dist/writers/parquet.d.ts +19 -0
  51. package/dist/writers/parquet.d.ts.map +1 -0
  52. package/dist/writers/parquet.js +129 -0
  53. package/dist/writers/parquet.js.map +1 -0
  54. package/package.json +13 -2
  55. package/skills/rowpipe/SKILL.md +181 -0
  56. package/benchmarks/memory-bench.ts +0 -109
  57. package/src/analytics/reduce.ts +0 -388
  58. package/src/analytics/schema-inference.ts +0 -217
  59. package/src/analytics/semantic-types.ts +0 -36
  60. package/src/analytics/stats.ts +0 -383
  61. package/src/analytics/validator.ts +0 -134
  62. package/src/cli/commands/cast.ts +0 -60
  63. package/src/cli/commands/convert.ts +0 -128
  64. package/src/cli/commands/filter.ts +0 -58
  65. package/src/cli/commands/inspect.ts +0 -193
  66. package/src/cli/commands/map.ts +0 -66
  67. package/src/cli/commands/reduce.ts +0 -74
  68. package/src/cli/commands/rename.ts +0 -59
  69. package/src/cli/commands/sample.ts +0 -61
  70. package/src/cli/commands/schema.ts +0 -86
  71. package/src/cli/commands/select.ts +0 -59
  72. package/src/cli/commands/stats.ts +0 -100
  73. package/src/cli/commands/validate.ts +0 -123
  74. package/src/cli/index.ts +0 -285
  75. package/src/core/batch.ts +0 -81
  76. package/src/core/errors.ts +0 -81
  77. package/src/core/pipeline.ts +0 -150
  78. package/src/core/types.ts +0 -110
  79. package/src/index.ts +0 -52
  80. package/src/readers/csv.ts +0 -302
  81. package/src/readers/index.ts +0 -88
  82. package/src/readers/json.ts +0 -256
  83. package/src/readers/jsonl.ts +0 -126
  84. package/src/readers/xlsx.ts +0 -182
  85. package/src/transforms/cast.ts +0 -212
  86. package/src/transforms/expression.ts +0 -1114
  87. package/src/transforms/filter.ts +0 -38
  88. package/src/transforms/map.ts +0 -99
  89. package/src/transforms/rename.ts +0 -47
  90. package/src/transforms/sample.ts +0 -57
  91. package/src/transforms/select.ts +0 -64
  92. package/src/utils/compression.ts +0 -43
  93. package/src/utils/formatting.ts +0 -75
  94. package/src/utils/progress.ts +0 -37
  95. package/src/writers/csv.ts +0 -106
  96. package/src/writers/index.ts +0 -87
  97. package/src/writers/json.ts +0 -52
  98. package/src/writers/jsonl.ts +0 -49
  99. package/src/writers/xlsx.ts +0 -87
  100. package/tests/analytics.test.ts +0 -119
  101. package/tests/cli-integration.test.ts +0 -187
  102. package/tests/map-reduce.test.ts +0 -223
  103. package/tests/readers-writers.test.ts +0 -189
  104. package/tests/transforms.test.ts +0 -199
  105. package/tsconfig.json +0 -21
  106. package/vitest.config.ts +0 -9
@@ -1,109 +0,0 @@
1
- import { createPipeline } from "../src/core/pipeline.js";
2
- import { filterRows } from "../src/transforms/filter.js";
3
- import { selectColumns } from "../src/transforms/select.js";
4
- import { castColumns } from "../src/transforms/cast.js";
5
- import { DatasetStatsAggregator } from "../src/analytics/stats.js";
6
- import type { DataBatch, DataStream, Row } from "../src/core/types.js";
7
- import { formatBytes, formatNumber } from "../src/utils/formatting.js";
8
-
9
- /**
10
- * Generator producing synthetic data batches without buffering.
11
- */
12
- async function* generateSyntheticStream(
13
- totalRows: number,
14
- batchSize = 2000
15
- ): DataStream {
16
- let offset = 0;
17
- while (offset < totalRows) {
18
- const currentBatchSize = Math.min(batchSize, totalRows - offset);
19
- const rows: Row[] = new Array(currentBatchSize);
20
-
21
- for (let i = 0; i < currentBatchSize; i++) {
22
- const id = offset + i;
23
- rows[i] = {
24
- id: String(id),
25
- name: `User_${id}`,
26
- age: String(20 + (id % 50)),
27
- email: `user${id}@example.com`,
28
- revenue: String((id % 1000) * 1.5),
29
- country: id % 3 === 0 ? "TR" : id % 3 === 1 ? "US" : "DE",
30
- };
31
- }
32
-
33
- yield {
34
- rows,
35
- offset,
36
- };
37
- offset += currentBatchSize;
38
- }
39
- }
40
-
41
- async function runBenchmark(totalRows: number): Promise<void> {
42
- process.stdout.write(`\n======================================================\n`);
43
- process.stdout.write(`Running Benchmark with ${formatNumber(totalRows)} synthetic rows...\n`);
44
- process.stdout.write(`Pipeline: Input -> Filter(age > 25) -> Select(id, name, age, revenue) -> Cast(revenue:number) -> Stats\n`);
45
- process.stdout.write(`------------------------------------------------------\n`);
46
-
47
- const initialMem = process.memoryUsage();
48
- let peakRss = initialMem.rss;
49
- let peakHeap = initialMem.heapUsed;
50
-
51
- const startTime = Date.now();
52
- let processedRows = 0;
53
-
54
- const rawStream = generateSyntheticStream(totalRows, 2000);
55
-
56
- const pipeline = createPipeline(rawStream)
57
- .pipe(filterRows("age > 25"))
58
- .pipe(selectColumns(["id", "name", "age", "revenue"]))
59
- .pipe(castColumns({ revenue: "number" }));
60
-
61
- const statsAgg = new DatasetStatsAggregator();
62
-
63
- for await (const batch of pipeline.batches()) {
64
- processedRows += batch.rows.length;
65
- for (const row of batch.rows) {
66
- statsAgg.add(row);
67
- }
68
-
69
- const currentMem = process.memoryUsage();
70
- if (currentMem.rss > peakRss) peakRss = currentMem.rss;
71
- if (currentMem.heapUsed > peakHeap) peakHeap = currentMem.heapUsed;
72
-
73
- if (processedRows % 250000 === 0 || processedRows === totalRows) {
74
- const elapsed = (Date.now() - startTime) / 1000 || 0.001;
75
- const speed = Math.round(processedRows / elapsed);
76
- process.stdout.write(
77
- ` Processed: ${formatNumber(processedRows)} rows | Elapsed: ${elapsed.toFixed(1)}s | RSS: ${formatBytes(currentMem.rss)} | Heap: ${formatBytes(currentMem.heapUsed)} | Speed: ${formatNumber(speed)} rows/s\n`
78
- );
79
- }
80
- }
81
-
82
- const totalTimeMs = Date.now() - startTime;
83
- const elapsedSec = totalTimeMs / 1000 || 0.001;
84
- const rowsPerSec = Math.round(totalRows / elapsedSec);
85
- // Estimate ~70 bytes per row
86
- const estimatedMB = (totalRows * 70) / (1024 * 1024);
87
- const mbPerSec = (estimatedMB / elapsedSec).toFixed(1);
88
-
89
- const results = statsAgg.result();
90
-
91
- process.stdout.write(`------------------------------------------------------\n`);
92
- process.stdout.write(`Benchmark Results:\n`);
93
- process.stdout.write(` Total Input Rows: ${formatNumber(totalRows)}\n`);
94
- process.stdout.write(` Filtered Rows: ${formatNumber(processedRows)}\n`);
95
- process.stdout.write(` Execution Time: ${elapsedSec.toFixed(2)}s\n`);
96
- process.stdout.write(` Throughput: ${formatNumber(rowsPerSec)} rows/s (~${mbPerSec} MB/s)\n`);
97
- process.stdout.write(` Initial RSS: ${formatBytes(initialMem.rss)}\n`);
98
- process.stdout.write(` Peak RSS: ${formatBytes(peakRss)}\n`);
99
- process.stdout.write(` Peak Heap Used: ${formatBytes(peakHeap)}\n`);
100
- process.stdout.write(` Memory Growth: Bounded ($O(1)$ constant memory)\n`);
101
- process.stdout.write(`======================================================\n`);
102
- }
103
-
104
- async function main() {
105
- await runBenchmark(100_000);
106
- await runBenchmark(1_000_000);
107
- }
108
-
109
- main().catch(console.error);
@@ -1,388 +0,0 @@
1
- import { InvalidArgumentError } from "../core/errors.js";
2
- import type { Aggregator, DataBatch, DataStream, Row, TransformFunction } from "../core/types.js";
3
- import { compileValueExpression } from "../transforms/expression.js";
4
- import { HyperLogLog } from "./stats.js";
5
-
6
- export type AggregationFunction =
7
- | "sum"
8
- | "avg"
9
- | "mean"
10
- | "min"
11
- | "max"
12
- | "count"
13
- | "countdistinct"
14
- | "stddev"
15
- | "variance"
16
- | "first"
17
- | "last";
18
-
19
- export interface AggregationSpec {
20
- targetField: string;
21
- func: AggregationFunction;
22
- sourceExpr?: string;
23
- }
24
-
25
- /**
26
- * Parses CLI reduce specs into structured AggregationSpec objects.
27
- * Supports syntax: "total_rev = sum(revenue)", "avg_margin = avg(margin)", "orders = count()"
28
- */
29
- export function parseReduceSpecs(specs: string[]): AggregationSpec[] {
30
- const result: AggregationSpec[] = [];
31
-
32
- for (const spec of specs) {
33
- const trimmed = spec.trim();
34
- if (!trimmed) continue;
35
-
36
- const eqIdx = trimmed.indexOf("=");
37
- if (eqIdx === -1) {
38
- throw new InvalidArgumentError(
39
- `Invalid reduce specification "${spec}". Expected format "target_column = func(source_expr)"`
40
- );
41
- }
42
-
43
- const targetField = trimmed.slice(0, eqIdx).trim();
44
- const rhs = trimmed.slice(eqIdx + 1).trim();
45
-
46
- const match = rhs.match(/^([a-zA-Z_]+)\s*\((.*)\)$/);
47
- if (!match) {
48
- throw new InvalidArgumentError(
49
- `Invalid aggregation function call in "${spec}". Expected format "func(column)" e.g. "sum(revenue)" or "count()"`
50
- );
51
- }
52
-
53
- const rawFunc = match[1]!.toLowerCase();
54
- const sourceExpr = match[2]!.trim() || undefined;
55
-
56
- const validFuncs = [
57
- "sum",
58
- "avg",
59
- "mean",
60
- "min",
61
- "max",
62
- "count",
63
- "countdistinct",
64
- "stddev",
65
- "variance",
66
- "first",
67
- "last",
68
- ];
69
-
70
- if (!validFuncs.includes(rawFunc)) {
71
- throw new InvalidArgumentError(
72
- `Unknown aggregation function "${rawFunc}" in "${spec}". Supported functions: ${validFuncs.join(", ")}`
73
- );
74
- }
75
-
76
- result.push({
77
- targetField,
78
- func: rawFunc as AggregationFunction,
79
- sourceExpr,
80
- });
81
- }
82
-
83
- return result;
84
- }
85
-
86
- interface Accumulator {
87
- add(val: unknown): void;
88
- result(): unknown;
89
- }
90
-
91
- class SumAccumulator implements Accumulator {
92
- private sum = 0;
93
- add(val: unknown): void {
94
- if (val === null || val === undefined || val === "") return;
95
- const num = typeof val === "number" ? val : Number(val);
96
- if (!Number.isNaN(num)) {
97
- this.sum += num;
98
- }
99
- }
100
- result(): number {
101
- return this.sum;
102
- }
103
- }
104
-
105
- class AvgAccumulator implements Accumulator {
106
- private sum = 0;
107
- private count = 0;
108
- add(val: unknown): void {
109
- if (val === null || val === undefined || val === "") return;
110
- const num = typeof val === "number" ? val : Number(val);
111
- if (!Number.isNaN(num)) {
112
- this.sum += num;
113
- this.count++;
114
- }
115
- }
116
- result(): number | null {
117
- return this.count > 0 ? this.sum / this.count : null;
118
- }
119
- }
120
-
121
- class MinAccumulator implements Accumulator {
122
- private min: number | null = null;
123
- add(val: unknown): void {
124
- if (val === null || val === undefined || val === "") return;
125
- const num = typeof val === "number" ? val : Number(val);
126
- if (!Number.isNaN(num)) {
127
- if (this.min === null || num < this.min) this.min = num;
128
- }
129
- }
130
- result(): number | null {
131
- return this.min;
132
- }
133
- }
134
-
135
- class MaxAccumulator implements Accumulator {
136
- private max: number | null = null;
137
- add(val: unknown): void {
138
- if (val === null || val === undefined || val === "") return;
139
- const num = typeof val === "number" ? val : Number(val);
140
- if (!Number.isNaN(num)) {
141
- if (this.max === null || num > this.max) this.max = num;
142
- }
143
- }
144
- result(): number | null {
145
- return this.max;
146
- }
147
- }
148
-
149
- class CountAccumulator implements Accumulator {
150
- private count = 0;
151
- private hasSource: boolean;
152
- constructor(hasSource: boolean) {
153
- this.hasSource = hasSource;
154
- }
155
- add(val: unknown): void {
156
- if (!this.hasSource) {
157
- this.count++;
158
- } else if (val !== null && val !== undefined && val !== "") {
159
- this.count++;
160
- }
161
- }
162
- result(): number {
163
- return this.count;
164
- }
165
- }
166
-
167
- class CountDistinctAccumulator implements Accumulator {
168
- private hll = new HyperLogLog();
169
- add(val: unknown): void {
170
- if (val !== null && val !== undefined && val !== "") {
171
- this.hll.add(val);
172
- }
173
- }
174
- result(): number {
175
- return this.hll.count();
176
- }
177
- }
178
-
179
- class WelfordAccumulator implements Accumulator {
180
- private count = 0;
181
- private mean = 0;
182
- private M2 = 0;
183
- private mode: "stddev" | "variance";
184
-
185
- constructor(mode: "stddev" | "variance") {
186
- this.mode = mode;
187
- }
188
-
189
- add(val: unknown): void {
190
- if (val === null || val === undefined || val === "") return;
191
- const num = typeof val === "number" ? val : Number(val);
192
- if (Number.isNaN(num)) return;
193
-
194
- this.count++;
195
- const delta = num - this.mean;
196
- this.mean += delta / this.count;
197
- const delta2 = num - this.mean;
198
- this.M2 += delta * delta2;
199
- }
200
-
201
- result(): number | null {
202
- if (this.count <= 1) return this.count === 1 ? 0 : null;
203
- const variance = this.M2 / (this.count - 1);
204
- return this.mode === "stddev" ? Math.sqrt(variance) : variance;
205
- }
206
- }
207
-
208
- class FirstAccumulator implements Accumulator {
209
- private value: unknown = null;
210
- private found = false;
211
- add(val: unknown): void {
212
- if (!this.found && val !== null && val !== undefined && val !== "") {
213
- this.value = val;
214
- this.found = true;
215
- }
216
- }
217
- result(): unknown {
218
- return this.value;
219
- }
220
- }
221
-
222
- class LastAccumulator implements Accumulator {
223
- private value: unknown = null;
224
- add(val: unknown): void {
225
- if (val !== null && val !== undefined && val !== "") {
226
- this.value = val;
227
- }
228
- }
229
- result(): unknown {
230
- return this.value;
231
- }
232
- }
233
-
234
- function createAccumulatorFor(func: AggregationFunction, hasSource: boolean): Accumulator {
235
- switch (func) {
236
- case "sum":
237
- return new SumAccumulator();
238
- case "avg":
239
- case "mean":
240
- return new AvgAccumulator();
241
- case "min":
242
- return new MinAccumulator();
243
- case "max":
244
- return new MaxAccumulator();
245
- case "count":
246
- return new CountAccumulator(hasSource);
247
- case "countdistinct":
248
- return new CountDistinctAccumulator();
249
- case "stddev":
250
- return new WelfordAccumulator("stddev");
251
- case "variance":
252
- return new WelfordAccumulator("variance");
253
- case "first":
254
- return new FirstAccumulator();
255
- case "last":
256
- return new LastAccumulator();
257
- }
258
- }
259
-
260
- interface CompiledAggregation {
261
- targetField: string;
262
- func: AggregationFunction;
263
- evaluator?: (row: Row) => unknown;
264
- createAccumulator: () => Accumulator;
265
- }
266
-
267
- export interface ReduceOptions {
268
- by?: string[];
269
- aggregations: AggregationSpec[] | string[];
270
- }
271
-
272
- /**
273
- * Streaming Reduce & Group-by Aggregator.
274
- */
275
- export class ReduceAggregator implements Aggregator<Row[]> {
276
- private byCols: string[];
277
- private compiled: CompiledAggregation[];
278
- private globalAccs?: Accumulator[];
279
- private groupMap?: Map<string, { groupValues: Record<string, unknown>; accs: Accumulator[] }>;
280
-
281
- constructor(options: ReduceOptions) {
282
- this.byCols = options.by && options.by.length > 0 ? options.by : [];
283
-
284
- const rawSpecs: AggregationSpec[] =
285
- typeof options.aggregations[0] === "string"
286
- ? parseReduceSpecs(options.aggregations as string[])
287
- : (options.aggregations as AggregationSpec[]);
288
-
289
- this.compiled = rawSpecs.map((spec) => ({
290
- targetField: spec.targetField,
291
- func: spec.func,
292
- evaluator: spec.sourceExpr ? compileValueExpression(spec.sourceExpr) : undefined,
293
- createAccumulator: () => createAccumulatorFor(spec.func, Boolean(spec.sourceExpr)),
294
- }));
295
-
296
- if (this.byCols.length === 0) {
297
- this.globalAccs = this.compiled.map((c) => c.createAccumulator());
298
- } else {
299
- this.groupMap = new Map();
300
- }
301
- }
302
-
303
- add(row: Row): void {
304
- if (this.globalAccs) {
305
- // Global reduction
306
- for (let i = 0; i < this.compiled.length; i++) {
307
- const item = this.compiled[i]!;
308
- const val = item.evaluator ? item.evaluator(row) : null;
309
- this.globalAccs[i]!.add(val);
310
- }
311
- } else if (this.groupMap) {
312
- // Group-by reduction
313
- let key = "";
314
- const groupValues: Record<string, unknown> = {};
315
-
316
- for (let i = 0; i < this.byCols.length; i++) {
317
- const col = this.byCols[i]!;
318
- const val = row[col] ?? null;
319
- groupValues[col] = val;
320
- key += (i > 0 ? "\x1f" : "") + String(val ?? "");
321
- }
322
-
323
- let group = this.groupMap.get(key);
324
- if (!group) {
325
- group = {
326
- groupValues,
327
- accs: this.compiled.map((c) => c.createAccumulator()),
328
- };
329
- this.groupMap.set(key, group);
330
- }
331
-
332
- for (let i = 0; i < this.compiled.length; i++) {
333
- const item = this.compiled[i]!;
334
- const val = item.evaluator ? item.evaluator(row) : null;
335
- group.accs[i]!.add(val);
336
- }
337
- }
338
- }
339
-
340
- result(): Row[] {
341
- if (this.globalAccs) {
342
- const summaryRow: Row = {};
343
- for (let i = 0; i < this.compiled.length; i++) {
344
- const item = this.compiled[i]!;
345
- summaryRow[item.targetField] = this.globalAccs[i]!.result();
346
- }
347
- return [summaryRow];
348
- }
349
-
350
- if (this.groupMap) {
351
- const rows: Row[] = [];
352
- for (const group of this.groupMap.values()) {
353
- const outRow: Row = { ...group.groupValues };
354
- for (let i = 0; i < this.compiled.length; i++) {
355
- const item = this.compiled[i]!;
356
- outRow[item.targetField] = group.accs[i]!.result();
357
- }
358
- rows.push(outRow);
359
- }
360
- return rows;
361
- }
362
-
363
- return [];
364
- }
365
- }
366
-
367
- /**
368
- * Creates a high-performance streaming transform that reduces/aggregates rows.
369
- */
370
- export function reduceRows(options: ReduceOptions): TransformFunction {
371
- return async function* (stream: DataStream): DataStream {
372
- const aggregator = new ReduceAggregator(options);
373
-
374
- for await (const batch of stream) {
375
- const len = batch.rows.length;
376
- for (let i = 0; i < len; i++) {
377
- aggregator.add(batch.rows[i]!);
378
- }
379
- }
380
-
381
- const finalRows = aggregator.result();
382
-
383
- yield {
384
- rows: finalRows,
385
- offset: 0,
386
- };
387
- };
388
- }
@@ -1,217 +0,0 @@
1
- import type { Aggregator, ColumnType, Row, SemanticType } from "../core/types.js";
2
- import { detectSemanticType } from "./semantic-types.js";
3
-
4
- export interface ColumnInferenceResult {
5
- name: string;
6
- type: ColumnType;
7
- nullable: boolean;
8
- confidence: number;
9
- semantic?: SemanticType;
10
- semanticConfidence?: number;
11
- typeBreakdown: Record<string, number>;
12
- sampleCount: number;
13
- nullCount: number;
14
- }
15
-
16
- export interface InferredSchemaResult {
17
- totalRowsScanned: number;
18
- columns: ColumnInferenceResult[];
19
- }
20
-
21
- const DATE_REGEX = /^\d{4}-\d{2}-\d{2}$/;
22
- const DATETIME_REGEX = /^\d{4}-\d{2}-\d{2}[T\s]\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:?\d{2})?$/;
23
-
24
- /**
25
- * Tests primitive type for an individual value.
26
- */
27
- export function classifyPrimitiveType(value: unknown): ColumnType {
28
- if (value === null || value === undefined || value === "") {
29
- return "null";
30
- }
31
-
32
- if (typeof value === "boolean") return "boolean";
33
- if (typeof value === "number") {
34
- return Number.isInteger(value) ? "integer" : "number";
35
- }
36
-
37
- const str = String(value).trim();
38
-
39
- // Boolean strings
40
- if (["true", "false"].includes(str.toLowerCase())) {
41
- return "boolean";
42
- }
43
-
44
- // Integer regex
45
- if (/^-?\d+$/.test(str)) {
46
- return "integer";
47
- }
48
-
49
- // Floating point regex
50
- if (/^-?\d+\.\d+$/.test(str) || /^-?\d+(?:\.\d+)?e[+-]?\d+$/i.test(str)) {
51
- return "number";
52
- }
53
-
54
- // Date / Datetime
55
- if (DATE_REGEX.test(str)) {
56
- const d = new Date(str);
57
- if (!Number.isNaN(d.getTime())) return "date";
58
- }
59
-
60
- if (DATETIME_REGEX.test(str)) {
61
- const d = new Date(str);
62
- if (!Number.isNaN(d.getTime())) return "datetime";
63
- }
64
-
65
- return "string";
66
- }
67
-
68
- export class SchemaInferenceAggregator implements Aggregator<InferredSchemaResult> {
69
- private totalRows = 0;
70
- private maxSample: number;
71
- private columnStats = new Map<
72
- string,
73
- {
74
- counts: Record<ColumnType, number>;
75
- semanticCounts: Record<string, number>;
76
- nullCount: number;
77
- totalCount: number;
78
- }
79
- >();
80
-
81
- constructor(options?: { sample?: number }) {
82
- this.maxSample = options?.sample ?? Number.POSITIVE_INFINITY;
83
- }
84
-
85
- add(row: Row): void {
86
- if (this.totalRows >= this.maxSample) return;
87
- this.totalRows++;
88
-
89
- for (const [col, val] of Object.entries(row)) {
90
- if (!this.columnStats.has(col)) {
91
- this.columnStats.set(col, {
92
- counts: {
93
- string: 0,
94
- integer: 0,
95
- number: 0,
96
- boolean: 0,
97
- date: 0,
98
- datetime: 0,
99
- null: 0,
100
- mixed: 0,
101
- },
102
- semanticCounts: {},
103
- nullCount: 0,
104
- totalCount: 0,
105
- });
106
- }
107
-
108
- const stat = this.columnStats.get(col)!;
109
- stat.totalCount++;
110
-
111
- const pType = classifyPrimitiveType(val);
112
- stat.counts[pType]++;
113
- if (pType === "null") {
114
- stat.nullCount++;
115
- } else {
116
- const sem = detectSemanticType(val);
117
- if (sem) {
118
- stat.semanticCounts[sem] = (stat.semanticCounts[sem] || 0) + 1;
119
- }
120
- }
121
- }
122
- }
123
-
124
- result(): InferredSchemaResult {
125
- const columns: ColumnInferenceResult[] = [];
126
-
127
- for (const [col, stat] of this.columnStats.entries()) {
128
- const nonNullCount = stat.totalCount - stat.nullCount;
129
- const nullable = stat.nullCount > 0;
130
-
131
- if (nonNullCount === 0) {
132
- columns.push({
133
- name: col,
134
- type: "null",
135
- nullable: true,
136
- confidence: 100,
137
- typeBreakdown: { null: 100 },
138
- sampleCount: stat.totalCount,
139
- nullCount: stat.nullCount,
140
- });
141
- continue;
142
- }
143
-
144
- // Calculate confidence percentages
145
- const breakdown: Record<string, number> = {};
146
- let dominantType: ColumnType = "string";
147
- let maxPercentage = 0;
148
-
149
- const candidates: ColumnType[] = [
150
- "integer",
151
- "number",
152
- "boolean",
153
- "datetime",
154
- "date",
155
- "string",
156
- ];
157
-
158
- for (const t of candidates) {
159
- const pct = (stat.counts[t] / nonNullCount) * 100;
160
- if (pct > 0) {
161
- breakdown[t] = Math.round(pct * 10) / 10;
162
- }
163
- if (pct > maxPercentage) {
164
- maxPercentage = pct;
165
- dominantType = t;
166
- }
167
- }
168
-
169
- // If integer + number together make 100%, consider number
170
- if (
171
- dominantType === "integer" &&
172
- stat.counts["number"] > 0 &&
173
- (stat.counts["integer"] + stat.counts["number"]) / nonNullCount >= 0.95
174
- ) {
175
- dominantType = "number";
176
- maxPercentage = ((stat.counts["integer"] + stat.counts["number"]) / nonNullCount) * 100;
177
- }
178
-
179
- let inferredType: ColumnType = dominantType;
180
- let confidence = Math.round(maxPercentage * 10) / 10;
181
-
182
- if (maxPercentage < 90 && Object.keys(breakdown).length > 1) {
183
- inferredType = "mixed";
184
- }
185
-
186
- // Semantic type detection
187
- let semantic: SemanticType | undefined;
188
- let semanticConfidence: number | undefined;
189
-
190
- for (const [sem, count] of Object.entries(stat.semanticCounts)) {
191
- const semPct = (count / nonNullCount) * 100;
192
- if (semPct >= 80) {
193
- semantic = sem as SemanticType;
194
- semanticConfidence = Math.round(semPct * 10) / 10;
195
- break;
196
- }
197
- }
198
-
199
- columns.push({
200
- name: col,
201
- type: inferredType,
202
- nullable,
203
- confidence,
204
- semantic,
205
- semanticConfidence,
206
- typeBreakdown: breakdown,
207
- sampleCount: stat.totalCount,
208
- nullCount: stat.nullCount,
209
- });
210
- }
211
-
212
- return {
213
- totalRowsScanned: this.totalRows,
214
- columns,
215
- };
216
- }
217
- }