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.
Files changed (185) hide show
  1. package/README.md +327 -41
  2. package/dist/cli/commands/diff.d.ts +30 -0
  3. package/dist/cli/commands/diff.d.ts.map +1 -0
  4. package/dist/cli/commands/diff.js +143 -0
  5. package/dist/cli/commands/diff.js.map +1 -0
  6. package/dist/cli/commands/files.d.ts +35 -0
  7. package/dist/cli/commands/files.d.ts.map +1 -0
  8. package/dist/cli/commands/files.js +93 -0
  9. package/dist/cli/commands/files.js.map +1 -0
  10. package/dist/cli/index.js +89 -5
  11. package/dist/cli/index.js.map +1 -1
  12. package/dist/core/errors.d.ts +23 -0
  13. package/dist/core/errors.d.ts.map +1 -1
  14. package/dist/core/errors.js +38 -0
  15. package/dist/core/errors.js.map +1 -1
  16. package/dist/core/types.d.ts +4 -0
  17. package/dist/core/types.d.ts.map +1 -1
  18. package/dist/diff/comparator.d.ts +17 -0
  19. package/dist/diff/comparator.d.ts.map +1 -0
  20. package/dist/diff/comparator.js +158 -0
  21. package/dist/diff/comparator.js.map +1 -0
  22. package/dist/diff/engine.d.ts +10 -0
  23. package/dist/diff/engine.d.ts.map +1 -0
  24. package/dist/diff/engine.js +333 -0
  25. package/dist/diff/engine.js.map +1 -0
  26. package/dist/diff/hash.d.ts +6 -0
  27. package/dist/diff/hash.d.ts.map +1 -0
  28. package/dist/diff/hash.js +50 -0
  29. package/dist/diff/hash.js.map +1 -0
  30. package/dist/diff/key.d.ts +21 -0
  31. package/dist/diff/key.d.ts.map +1 -0
  32. package/dist/diff/key.js +107 -0
  33. package/dist/diff/key.js.map +1 -0
  34. package/dist/diff/reporter.d.ts +18 -0
  35. package/dist/diff/reporter.d.ts.map +1 -0
  36. package/dist/diff/reporter.js +133 -0
  37. package/dist/diff/reporter.js.map +1 -0
  38. package/dist/diff/schema.d.ts +7 -0
  39. package/dist/diff/schema.d.ts.map +1 -0
  40. package/dist/diff/schema.js +65 -0
  41. package/dist/diff/schema.js.map +1 -0
  42. package/dist/diff/storage/disk-index.d.ts +28 -0
  43. package/dist/diff/storage/disk-index.d.ts.map +1 -0
  44. package/dist/diff/storage/disk-index.js +105 -0
  45. package/dist/diff/storage/disk-index.js.map +1 -0
  46. package/dist/diff/storage/memory-index.d.ts +23 -0
  47. package/dist/diff/storage/memory-index.d.ts.map +1 -0
  48. package/dist/diff/storage/memory-index.js +69 -0
  49. package/dist/diff/storage/memory-index.js.map +1 -0
  50. package/dist/diff/storage/spillable-index.d.ts +30 -0
  51. package/dist/diff/storage/spillable-index.d.ts.map +1 -0
  52. package/dist/diff/storage/spillable-index.js +108 -0
  53. package/dist/diff/storage/spillable-index.js.map +1 -0
  54. package/dist/diff/types.d.ts +127 -0
  55. package/dist/diff/types.d.ts.map +1 -0
  56. package/dist/diff/types.js +2 -0
  57. package/dist/diff/types.js.map +1 -0
  58. package/dist/files/glob.d.ts +21 -0
  59. package/dist/files/glob.d.ts.map +1 -0
  60. package/dist/files/glob.js +132 -0
  61. package/dist/files/glob.js.map +1 -0
  62. package/dist/files/hash.d.ts +10 -0
  63. package/dist/files/hash.d.ts.map +1 -0
  64. package/dist/files/hash.js +67 -0
  65. package/dist/files/hash.js.map +1 -0
  66. package/dist/files/index.d.ts +6 -0
  67. package/dist/files/index.d.ts.map +1 -0
  68. package/dist/files/index.js +6 -0
  69. package/dist/files/index.js.map +1 -0
  70. package/dist/files/mime.d.ts +9 -0
  71. package/dist/files/mime.d.ts.map +1 -0
  72. package/dist/files/mime.js +128 -0
  73. package/dist/files/mime.js.map +1 -0
  74. package/dist/files/reader.d.ts +19 -0
  75. package/dist/files/reader.d.ts.map +1 -0
  76. package/dist/files/reader.js +344 -0
  77. package/dist/files/reader.js.map +1 -0
  78. package/dist/files/types.d.ts +47 -0
  79. package/dist/files/types.d.ts.map +1 -0
  80. package/dist/files/types.js +2 -0
  81. package/dist/files/types.js.map +1 -0
  82. package/dist/index.d.ts +17 -2
  83. package/dist/index.d.ts.map +1 -1
  84. package/dist/index.js +19 -2
  85. package/dist/index.js.map +1 -1
  86. package/dist/readers/csv.d.ts.map +1 -1
  87. package/dist/readers/csv.js +59 -27
  88. package/dist/readers/csv.js.map +1 -1
  89. package/dist/readers/index.d.ts +2 -0
  90. package/dist/readers/index.d.ts.map +1 -1
  91. package/dist/readers/index.js +57 -3
  92. package/dist/readers/index.js.map +1 -1
  93. package/dist/readers/json.d.ts.map +1 -1
  94. package/dist/readers/json.js +6 -8
  95. package/dist/readers/json.js.map +1 -1
  96. package/dist/readers/jsonl.d.ts.map +1 -1
  97. package/dist/readers/jsonl.js +5 -7
  98. package/dist/readers/jsonl.js.map +1 -1
  99. package/dist/readers/parquet.d.ts +17 -0
  100. package/dist/readers/parquet.d.ts.map +1 -0
  101. package/dist/readers/parquet.js +146 -0
  102. package/dist/readers/parquet.js.map +1 -0
  103. package/dist/transforms/expression.d.ts.map +1 -1
  104. package/dist/transforms/expression.js +73 -7
  105. package/dist/transforms/expression.js.map +1 -1
  106. package/dist/utils/compression.d.ts +30 -6
  107. package/dist/utils/compression.d.ts.map +1 -1
  108. package/dist/utils/compression.js +95 -18
  109. package/dist/utils/compression.js.map +1 -1
  110. package/dist/writers/csv.d.ts.map +1 -1
  111. package/dist/writers/csv.js +29 -17
  112. package/dist/writers/csv.js.map +1 -1
  113. package/dist/writers/index.d.ts +2 -0
  114. package/dist/writers/index.d.ts.map +1 -1
  115. package/dist/writers/index.js +49 -4
  116. package/dist/writers/index.js.map +1 -1
  117. package/dist/writers/json.d.ts +1 -1
  118. package/dist/writers/json.d.ts.map +1 -1
  119. package/dist/writers/json.js +9 -11
  120. package/dist/writers/json.js.map +1 -1
  121. package/dist/writers/jsonl.d.ts +1 -1
  122. package/dist/writers/jsonl.d.ts.map +1 -1
  123. package/dist/writers/jsonl.js +9 -11
  124. package/dist/writers/jsonl.js.map +1 -1
  125. package/dist/writers/markdown.d.ts +21 -0
  126. package/dist/writers/markdown.d.ts.map +1 -0
  127. package/dist/writers/markdown.js +82 -0
  128. package/dist/writers/markdown.js.map +1 -0
  129. package/dist/writers/parquet.d.ts +19 -0
  130. package/dist/writers/parquet.d.ts.map +1 -0
  131. package/dist/writers/parquet.js +129 -0
  132. package/dist/writers/parquet.js.map +1 -0
  133. package/package.json +16 -3
  134. package/skills/rowpipe/SKILL.md +263 -0
  135. package/benchmarks/memory-bench.ts +0 -109
  136. package/src/analytics/reduce.ts +0 -388
  137. package/src/analytics/schema-inference.ts +0 -217
  138. package/src/analytics/semantic-types.ts +0 -36
  139. package/src/analytics/stats.ts +0 -383
  140. package/src/analytics/validator.ts +0 -134
  141. package/src/cli/commands/cast.ts +0 -60
  142. package/src/cli/commands/convert.ts +0 -128
  143. package/src/cli/commands/filter.ts +0 -58
  144. package/src/cli/commands/inspect.ts +0 -193
  145. package/src/cli/commands/map.ts +0 -66
  146. package/src/cli/commands/reduce.ts +0 -74
  147. package/src/cli/commands/rename.ts +0 -59
  148. package/src/cli/commands/sample.ts +0 -61
  149. package/src/cli/commands/schema.ts +0 -86
  150. package/src/cli/commands/select.ts +0 -59
  151. package/src/cli/commands/stats.ts +0 -100
  152. package/src/cli/commands/validate.ts +0 -123
  153. package/src/cli/index.ts +0 -285
  154. package/src/core/batch.ts +0 -81
  155. package/src/core/errors.ts +0 -81
  156. package/src/core/pipeline.ts +0 -150
  157. package/src/core/types.ts +0 -110
  158. package/src/index.ts +0 -52
  159. package/src/readers/csv.ts +0 -302
  160. package/src/readers/index.ts +0 -88
  161. package/src/readers/json.ts +0 -256
  162. package/src/readers/jsonl.ts +0 -126
  163. package/src/readers/xlsx.ts +0 -182
  164. package/src/transforms/cast.ts +0 -212
  165. package/src/transforms/expression.ts +0 -1114
  166. package/src/transforms/filter.ts +0 -38
  167. package/src/transforms/map.ts +0 -99
  168. package/src/transforms/rename.ts +0 -47
  169. package/src/transforms/sample.ts +0 -57
  170. package/src/transforms/select.ts +0 -64
  171. package/src/utils/compression.ts +0 -43
  172. package/src/utils/formatting.ts +0 -75
  173. package/src/utils/progress.ts +0 -37
  174. package/src/writers/csv.ts +0 -106
  175. package/src/writers/index.ts +0 -87
  176. package/src/writers/json.ts +0 -52
  177. package/src/writers/jsonl.ts +0 -49
  178. package/src/writers/xlsx.ts +0 -87
  179. package/tests/analytics.test.ts +0 -119
  180. package/tests/cli-integration.test.ts +0 -187
  181. package/tests/map-reduce.test.ts +0 -223
  182. package/tests/readers-writers.test.ts +0 -189
  183. package/tests/transforms.test.ts +0 -199
  184. package/tsconfig.json +0 -21
  185. package/vitest.config.ts +0 -9
@@ -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
- }
@@ -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
- }