tranfi 0.0.2 → 0.1.2

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 (108) hide show
  1. package/README.md +395 -0
  2. package/app/assets/index-6quYZ5Ap.css +5 -0
  3. package/app/assets/index-pDFMluyz.js +160 -0
  4. package/app/assets/materialdesignicons-webfont-B7mPwVP_.ttf +0 -0
  5. package/app/assets/materialdesignicons-webfont-CSr8KVlo.eot +0 -0
  6. package/app/assets/materialdesignicons-webfont-Dp5v-WZN.woff2 +0 -0
  7. package/app/assets/materialdesignicons-webfont-PXm3-2wK.woff +0 -0
  8. package/app/index.html +13 -0
  9. package/binding.gyp +69 -0
  10. package/csrc/arena.c +91 -0
  11. package/csrc/batch.c +229 -0
  12. package/csrc/buffer.c +78 -0
  13. package/csrc/cJSON.c +3143 -0
  14. package/csrc/cJSON.h +300 -0
  15. package/csrc/codec_csv.c +1058 -0
  16. package/csrc/codec_jsonl.c +374 -0
  17. package/csrc/codec_table.c +218 -0
  18. package/csrc/codec_text.c +229 -0
  19. package/csrc/compiler.c +102 -0
  20. package/csrc/date_utils.h +94 -0
  21. package/csrc/dsl.c +1180 -0
  22. package/csrc/dsl.h +22 -0
  23. package/csrc/expr.c +1245 -0
  24. package/csrc/expr.h +56 -0
  25. package/csrc/internal.h +250 -0
  26. package/csrc/ir.c +119 -0
  27. package/csrc/ir.h +167 -0
  28. package/csrc/ir_schema.c +60 -0
  29. package/csrc/ir_serialize.c +104 -0
  30. package/csrc/ir_sql.c +1211 -0
  31. package/csrc/ir_validate.c +120 -0
  32. package/csrc/main.c +392 -0
  33. package/csrc/op_acf.c +133 -0
  34. package/csrc/op_anomaly.c +120 -0
  35. package/csrc/op_bin.c +109 -0
  36. package/csrc/op_cast.c +195 -0
  37. package/csrc/op_clip.c +88 -0
  38. package/csrc/op_date_trunc.c +181 -0
  39. package/csrc/op_datetime.c +212 -0
  40. package/csrc/op_derive.c +248 -0
  41. package/csrc/op_diff.c +134 -0
  42. package/csrc/op_ewma.c +103 -0
  43. package/csrc/op_explode.c +108 -0
  44. package/csrc/op_fill_down.c +163 -0
  45. package/csrc/op_fill_null.c +123 -0
  46. package/csrc/op_filter.c +132 -0
  47. package/csrc/op_frequency.c +193 -0
  48. package/csrc/op_grep.c +163 -0
  49. package/csrc/op_group_agg.c +285 -0
  50. package/csrc/op_hash.c +126 -0
  51. package/csrc/op_head.c +149 -0
  52. package/csrc/op_interpolate.c +239 -0
  53. package/csrc/op_join.c +384 -0
  54. package/csrc/op_label_encode.c +144 -0
  55. package/csrc/op_lead.c +190 -0
  56. package/csrc/op_normalize.c +226 -0
  57. package/csrc/op_onehot.c +185 -0
  58. package/csrc/op_pivot.c +370 -0
  59. package/csrc/op_registry.c +1148 -0
  60. package/csrc/op_rename.c +138 -0
  61. package/csrc/op_replace.c +202 -0
  62. package/csrc/op_sample.c +101 -0
  63. package/csrc/op_select.c +140 -0
  64. package/csrc/op_skip.c +152 -0
  65. package/csrc/op_sort.c +273 -0
  66. package/csrc/op_split.c +114 -0
  67. package/csrc/op_split_data.c +87 -0
  68. package/csrc/op_stack.c +315 -0
  69. package/csrc/op_stats.c +779 -0
  70. package/csrc/op_step.c +171 -0
  71. package/csrc/op_tail.c +96 -0
  72. package/csrc/op_top.c +150 -0
  73. package/csrc/op_trim.c +109 -0
  74. package/csrc/op_unique.c +300 -0
  75. package/csrc/op_unpivot.c +159 -0
  76. package/csrc/op_validate.c +71 -0
  77. package/csrc/op_window.c +150 -0
  78. package/csrc/pipeline.c +315 -0
  79. package/csrc/plan.c +206 -0
  80. package/csrc/recipes.c +102 -0
  81. package/csrc/recipes.h +27 -0
  82. package/csrc/report.c +463 -0
  83. package/csrc/report.h +22 -0
  84. package/csrc/tranfi.h +123 -0
  85. package/csrc/wasm_api.c +157 -0
  86. package/napi_api.c +326 -0
  87. package/package.json +46 -57
  88. package/src/cli.js +193 -0
  89. package/src/engines/duckdb.js +109 -0
  90. package/src/index.js +306 -0
  91. package/src/native.js +22 -0
  92. package/src/pipeline.js +286 -0
  93. package/src/server.js +277 -0
  94. package/src/wasm.js +19 -0
  95. package/wasm/index.js +244 -0
  96. package/wasm/package.json +1 -0
  97. package/wasm/tranfi_core.js +0 -0
  98. package/LICENSE +0 -21
  99. package/dist/bundle.js +0 -1
  100. package/index.html +0 -18
  101. package/src/app.css +0 -169
  102. package/src/app.js +0 -203
  103. package/src/app.vue +0 -250
  104. package/src/bulma-input.vue +0 -110
  105. package/src/common-inputs.js +0 -28
  106. package/src/main.js +0 -20
  107. package/src/transforms.js +0 -166
  108. package/webpack.config.js +0 -108
@@ -0,0 +1,779 @@
1
+ /*
2
+ * op_stats.c — Streaming aggregates with online algorithms.
3
+ *
4
+ * Config: {"stats": ["count","sum","avg","min","max","var","stddev",
5
+ * "median","p25","p75","skewness","kurtosis",
6
+ * "distinct","hist","sample"]}
7
+ * or {} for default stats (count, sum, avg, min, max, var, stddev, median).
8
+ *
9
+ * Algorithms ported from OnlineStats.jl (MIT):
10
+ * - Welford's online variance (var, stddev)
11
+ * - Moments via non-central moment tracking (skewness, kurtosis)
12
+ * - P2 quantile — Jain-Chlamtac (median, p25, p75)
13
+ * - HyperLogLog cardinality estimation (distinct)
14
+ * - Streaming histogram with adaptive bins (hist)
15
+ * - Reservoir sampling — Algorithm R (sample)
16
+ *
17
+ * Output: long format, one row per input column.
18
+ */
19
+
20
+ #include "internal.h"
21
+ #include "cJSON.h"
22
+ #include "date_utils.h"
23
+ #include <stdlib.h>
24
+ #include <string.h>
25
+ #include <stdio.h>
26
+ #include <float.h>
27
+ #include <math.h>
28
+
29
+ /* ---- P2 Quantile (Jain-Chlamtac) ---- */
30
+
31
+ typedef struct {
32
+ double q[5]; /* marker heights */
33
+ int n[5]; /* marker positions */
34
+ double np[5]; /* desired positions */
35
+ double dn[5]; /* increment for desired positions */
36
+ double tau; /* target quantile */
37
+ int nobs;
38
+ double init[5]; /* first 5 observations for init */
39
+ } p2_quantile;
40
+
41
+ static void p2_init(p2_quantile *p, double tau) {
42
+ memset(p, 0, sizeof(*p));
43
+ p->tau = tau;
44
+ p->dn[0] = 0;
45
+ p->dn[1] = tau / 2.0;
46
+ p->dn[2] = tau;
47
+ p->dn[3] = (1.0 + tau) / 2.0;
48
+ p->dn[4] = 1.0;
49
+ }
50
+
51
+ static int dbl_cmp(const void *a, const void *b) {
52
+ double da = *(const double *)a, db = *(const double *)b;
53
+ return (da > db) - (da < db);
54
+ }
55
+
56
+ static void p2_interpolate(double *q, int *n, int i, int d) {
57
+ double df = (double)d;
58
+ double qi = q[i-1], qm = q[i], qp = q[i+1];
59
+ int ni = n[i-1], nm = n[i], np = n[i+1];
60
+
61
+ double v1 = (double)(nm - ni + d) * (qp - qm) / (double)(np - nm);
62
+ double v2 = (double)(np - nm - d) * (qm - qi) / (double)(nm - ni);
63
+ double qnew = qm + df / (double)(np - ni) * (v1 + v2);
64
+
65
+ if (qi < qnew && qnew < qp) {
66
+ q[i] = qnew;
67
+ } else {
68
+ /* linear fallback */
69
+ q[i] = qm + df * (q[i + d] - qm) / (double)(n[i + d] - nm);
70
+ }
71
+ n[i] += d;
72
+ }
73
+
74
+ static void p2_update(p2_quantile *p, double y) {
75
+ p->nobs++;
76
+ if (p->nobs <= 5) {
77
+ p->init[p->nobs - 1] = y;
78
+ if (p->nobs == 5) {
79
+ qsort(p->init, 5, sizeof(double), dbl_cmp);
80
+ for (int i = 0; i < 5; i++) {
81
+ p->q[i] = p->init[i];
82
+ p->n[i] = i + 1;
83
+ }
84
+ p->np[0] = 1;
85
+ p->np[1] = 1 + 2 * p->tau;
86
+ p->np[2] = 1 + 4 * p->tau;
87
+ p->np[3] = 3 + 2 * p->tau;
88
+ p->np[4] = 5;
89
+ }
90
+ return;
91
+ }
92
+
93
+ /* Find cell k */
94
+ int k;
95
+ if (y < p->q[0]) {
96
+ p->q[0] = y;
97
+ k = 0;
98
+ } else if (y >= p->q[4]) {
99
+ p->q[4] = y > p->q[4] ? y : p->q[4];
100
+ k = 3;
101
+ } else {
102
+ k = 0;
103
+ for (int i = 1; i < 5; i++) {
104
+ if (y < p->q[i]) { k = i - 1; break; }
105
+ }
106
+ }
107
+
108
+ /* Increment positions */
109
+ for (int i = k + 1; i < 5; i++) p->n[i]++;
110
+ for (int i = 0; i < 5; i++) p->np[i] += p->dn[i];
111
+
112
+ /* Adjust markers 1-3 */
113
+ for (int i = 1; i <= 3; i++) {
114
+ double di = p->np[i] - (double)p->n[i];
115
+ if ((di >= 1.0 && p->n[i+1] - p->n[i] > 1) ||
116
+ (di <= -1.0 && p->n[i-1] - p->n[i] < -1)) {
117
+ int d = di >= 0 ? 1 : -1;
118
+ p2_interpolate(p->q, p->n, i, d);
119
+ }
120
+ }
121
+ }
122
+
123
+ static double p2_value(const p2_quantile *p) {
124
+ if (p->nobs < 5) {
125
+ /* Not enough data; sort what we have and pick quantile */
126
+ if (p->nobs == 0) return NAN;
127
+ double tmp[5];
128
+ memcpy(tmp, p->init, p->nobs * sizeof(double));
129
+ qsort(tmp, p->nobs, sizeof(double), dbl_cmp);
130
+ size_t idx = (size_t)(p->tau * (p->nobs - 1));
131
+ return tmp[idx];
132
+ }
133
+ return p->q[2]; /* middle marker */
134
+ }
135
+
136
+ /* ---- HyperLogLog ---- */
137
+
138
+ #define HLL_P 10
139
+ #define HLL_M (1 << HLL_P) /* 1024 registers */
140
+
141
+ typedef struct {
142
+ uint8_t M[HLL_M];
143
+ size_t n;
144
+ } hll_state;
145
+
146
+ static void hll_init(hll_state *h) {
147
+ memset(h, 0, sizeof(*h));
148
+ }
149
+
150
+ static uint32_t hll_hash(const char *key) {
151
+ /* MurmurHash3 finalizer-style mixing */
152
+ uint32_t h = 0x811c9dc5u;
153
+ for (const char *p = key; *p; p++) {
154
+ h ^= (uint32_t)(unsigned char)*p;
155
+ h *= 0x01000193u;
156
+ }
157
+ h ^= h >> 16;
158
+ h *= 0x85ebca6bu;
159
+ h ^= h >> 13;
160
+ h *= 0xc2b2ae35u;
161
+ h ^= h >> 16;
162
+ return h;
163
+ }
164
+
165
+ static int hll_clz32(uint32_t x) {
166
+ if (x == 0) return 32;
167
+ #if defined(__GNUC__) || defined(__clang__)
168
+ return __builtin_clz(x);
169
+ #else
170
+ int n = 0;
171
+ if (!(x & 0xFFFF0000u)) { n += 16; x <<= 16; }
172
+ if (!(x & 0xFF000000u)) { n += 8; x <<= 8; }
173
+ if (!(x & 0xF0000000u)) { n += 4; x <<= 4; }
174
+ if (!(x & 0xC0000000u)) { n += 2; x <<= 2; }
175
+ if (!(x & 0x80000000u)) { n += 1; }
176
+ return n;
177
+ #endif
178
+ }
179
+
180
+ static void hll_update(hll_state *h, const char *val) {
181
+ h->n++;
182
+ uint32_t x = hll_hash(val);
183
+ uint32_t idx = x & (HLL_M - 1);
184
+ uint32_t w = x >> HLL_P;
185
+ int rho = hll_clz32(w) + 1;
186
+ if (rho > 32 - HLL_P) rho = 32 - HLL_P;
187
+ if ((uint8_t)rho > h->M[idx]) h->M[idx] = (uint8_t)rho;
188
+ }
189
+
190
+ static double hll_estimate(const hll_state *h) {
191
+ double alpha;
192
+ if (HLL_P == 4) alpha = 0.673;
193
+ else if (HLL_P == 5) alpha = 0.697;
194
+ else if (HLL_P == 6) alpha = 0.709;
195
+ else alpha = 0.7213 / (1.0 + 1.079 / HLL_M);
196
+
197
+ double sum = 0;
198
+ int V = 0;
199
+ for (int i = 0; i < HLL_M; i++) {
200
+ sum += 1.0 / (1u << h->M[i]);
201
+ if (h->M[i] == 0) V++;
202
+ }
203
+ double E = alpha * HLL_M * HLL_M / sum;
204
+
205
+ /* Small/large range corrections */
206
+ if (E <= 2.5 * HLL_M) {
207
+ if (V > 0) return HLL_M * log((double)HLL_M / V);
208
+ return E;
209
+ }
210
+ if (E <= (1.0 / 30.0) * 4294967296.0) return E;
211
+ return -4294967296.0 * log(1.0 - E / 4294967296.0);
212
+ }
213
+
214
+ /* ---- Streaming Histogram (adaptive bins) ---- */
215
+
216
+ #define HIST_NBINS 32
217
+
218
+ typedef struct {
219
+ double edges[HIST_NBINS + 1];
220
+ size_t counts[HIST_NBINS];
221
+ size_t n;
222
+ double lo, hi;
223
+ int initialized;
224
+ } hist_state;
225
+
226
+ static void hist_init(hist_state *h) {
227
+ memset(h, 0, sizeof(*h));
228
+ }
229
+
230
+ static void hist_setup_edges(hist_state *h) {
231
+ double step = (h->hi - h->lo) / HIST_NBINS;
232
+ for (int i = 0; i <= HIST_NBINS; i++)
233
+ h->edges[i] = h->lo + step * i;
234
+ }
235
+
236
+ static int hist_bin(const hist_state *h, double y) {
237
+ if (y < h->edges[0]) return -1;
238
+ if (y >= h->edges[HIST_NBINS]) return HIST_NBINS - 1; /* last bin is closed */
239
+ /* binary search */
240
+ int lo = 0, hi = HIST_NBINS - 1;
241
+ while (lo < hi) {
242
+ int mid = (lo + hi) / 2;
243
+ if (y < h->edges[mid + 1]) hi = mid;
244
+ else lo = mid + 1;
245
+ }
246
+ return lo;
247
+ }
248
+
249
+ static void hist_expand(hist_state *h, double y) {
250
+ if (y >= h->lo && y <= h->hi) return;
251
+
252
+ if (y > h->hi) {
253
+ /* Expand right: double range until y fits */
254
+ while (y > h->hi) {
255
+ double w = h->hi - h->lo;
256
+ h->hi = h->lo + w * 2.0;
257
+ /* Merge bin pairs: bins [0,1]→0, [2,3]→1, etc. */
258
+ for (int i = 0; i < HIST_NBINS / 2; i++)
259
+ h->counts[i] = h->counts[2*i] + h->counts[2*i + 1];
260
+ for (int i = HIST_NBINS / 2; i < HIST_NBINS; i++)
261
+ h->counts[i] = 0;
262
+ hist_setup_edges(h);
263
+ }
264
+ } else {
265
+ /* Expand left */
266
+ while (y < h->lo) {
267
+ double w = h->hi - h->lo;
268
+ h->lo = h->hi - w * 2.0;
269
+ for (int i = HIST_NBINS - 1; i >= HIST_NBINS / 2; i--)
270
+ h->counts[i] = h->counts[2*(i - HIST_NBINS/2)] +
271
+ h->counts[2*(i - HIST_NBINS/2) + 1];
272
+ for (int i = 0; i < HIST_NBINS / 2; i++)
273
+ h->counts[i] = 0;
274
+ hist_setup_edges(h);
275
+ }
276
+ }
277
+ }
278
+
279
+ static void hist_update(hist_state *h, double y) {
280
+ h->n++;
281
+ if (!h->initialized) {
282
+ h->lo = y;
283
+ h->hi = y;
284
+ h->initialized = 1;
285
+ return; /* single value, no bin yet */
286
+ }
287
+ if (h->lo == h->hi) {
288
+ /* Second distinct value */
289
+ if (y == h->lo) return;
290
+ if (y < h->lo) h->lo = y; else h->hi = y;
291
+ /* Put prior observations in appropriate bin */
292
+ double range = h->hi - h->lo;
293
+ h->lo -= range * 0.01;
294
+ h->hi += range * 0.01;
295
+ hist_setup_edges(h);
296
+ /* All prior n-1 points were the same value, assign to their bin */
297
+ int b = hist_bin(h, h->lo + range * 0.01 + range * 0.5);
298
+ if (b >= 0 && b < HIST_NBINS) h->counts[b] = h->n - 1;
299
+ b = hist_bin(h, y);
300
+ if (b >= 0 && b < HIST_NBINS) h->counts[b]++;
301
+ return;
302
+ }
303
+
304
+ hist_expand(h, y);
305
+ int b = hist_bin(h, y);
306
+ if (b >= 0 && b < HIST_NBINS) h->counts[b]++;
307
+ }
308
+
309
+ /* ---- Reservoir Sample (Algorithm R) ---- */
310
+
311
+ #define RESERVOIR_K 10
312
+
313
+ typedef struct {
314
+ double values[RESERVOIR_K];
315
+ size_t n;
316
+ /* Simple xorshift RNG state */
317
+ uint64_t rng;
318
+ } reservoir_state;
319
+
320
+ static void reservoir_init(reservoir_state *r) {
321
+ memset(r, 0, sizeof(*r));
322
+ r->rng = 0x12345678deadbeefULL;
323
+ }
324
+
325
+ static uint64_t reservoir_rand(reservoir_state *r) {
326
+ r->rng ^= r->rng << 13;
327
+ r->rng ^= r->rng >> 7;
328
+ r->rng ^= r->rng << 17;
329
+ return r->rng;
330
+ }
331
+
332
+ static void reservoir_update(reservoir_state *r, double y) {
333
+ r->n++;
334
+ if (r->n <= RESERVOIR_K) {
335
+ r->values[r->n - 1] = y;
336
+ } else {
337
+ uint64_t j = reservoir_rand(r) % r->n;
338
+ if (j < RESERVOIR_K) {
339
+ r->values[j] = y;
340
+ }
341
+ }
342
+ }
343
+
344
+ /* ---- Per-column accumulator ---- */
345
+
346
+ typedef struct {
347
+ /* Basic stats */
348
+ size_t count;
349
+ double sum;
350
+ double min;
351
+ double max;
352
+ int has_numeric;
353
+
354
+ /* Welford's variance */
355
+ double wf_mean;
356
+ double wf_m2; /* sum of (x - mean)^2 */
357
+
358
+ /* Moments (non-central) for skewness/kurtosis */
359
+ double mom[4]; /* E[X], E[X^2], E[X^3], E[X^4] */
360
+
361
+ /* P2 quantiles */
362
+ p2_quantile p2_median;
363
+ p2_quantile p2_p25;
364
+ p2_quantile p2_p75;
365
+
366
+ /* HyperLogLog */
367
+ hll_state *hll;
368
+
369
+ /* Histogram */
370
+ hist_state *hist;
371
+
372
+ /* Reservoir sample */
373
+ reservoir_state *reservoir;
374
+ } col_accum;
375
+
376
+ /* ---- Stats state ---- */
377
+
378
+ typedef struct {
379
+ col_accum *accums;
380
+ char **col_names;
381
+ size_t n_cols;
382
+ int initialized;
383
+ /* Which stats to output */
384
+ int want_count, want_sum, want_avg, want_min, want_max;
385
+ int want_var, want_stddev;
386
+ int want_median, want_p25, want_p75;
387
+ int want_skewness, want_kurtosis;
388
+ int want_distinct;
389
+ int want_hist;
390
+ int want_sample;
391
+ } stats_state;
392
+
393
+ static void accum_init(col_accum *a, const stats_state *st) {
394
+ a->min = DBL_MAX;
395
+ a->max = -DBL_MAX;
396
+ p2_init(&a->p2_median, 0.5);
397
+ p2_init(&a->p2_p25, 0.25);
398
+ p2_init(&a->p2_p75, 0.75);
399
+
400
+ if (st->want_distinct) {
401
+ a->hll = calloc(1, sizeof(hll_state));
402
+ if (a->hll) hll_init(a->hll);
403
+ }
404
+ if (st->want_hist) {
405
+ a->hist = calloc(1, sizeof(hist_state));
406
+ if (a->hist) hist_init(a->hist);
407
+ }
408
+ if (st->want_sample) {
409
+ a->reservoir = calloc(1, sizeof(reservoir_state));
410
+ if (a->reservoir) reservoir_init(a->reservoir);
411
+ }
412
+ }
413
+
414
+ static void accum_update(col_accum *a, double val, const char *str_val,
415
+ int is_num) {
416
+ a->count++;
417
+ if (is_num) {
418
+ a->has_numeric = 1;
419
+ a->sum += val;
420
+ if (val < a->min) a->min = val;
421
+ if (val > a->max) a->max = val;
422
+
423
+ /* Welford */
424
+ double old_mean = a->wf_mean;
425
+ a->wf_mean += (val - a->wf_mean) / (double)a->count;
426
+ a->wf_m2 += (val - a->wf_mean) * (val - old_mean);
427
+
428
+ /* Moments */
429
+ double gamma = 1.0 / (double)a->count;
430
+ double y2 = val * val;
431
+ a->mom[0] += gamma * (val - a->mom[0]);
432
+ a->mom[1] += gamma * (y2 - a->mom[1]);
433
+ a->mom[2] += gamma * (val * y2 - a->mom[2]);
434
+ a->mom[3] += gamma * (y2 * y2 - a->mom[3]);
435
+
436
+ /* P2 quantiles */
437
+ p2_update(&a->p2_median, val);
438
+ p2_update(&a->p2_p25, val);
439
+ p2_update(&a->p2_p75, val);
440
+
441
+ /* Histogram */
442
+ if (a->hist) hist_update(a->hist, val);
443
+
444
+ /* Reservoir */
445
+ if (a->reservoir) reservoir_update(a->reservoir, val);
446
+ }
447
+
448
+ /* HyperLogLog uses string representation for all types */
449
+ if (a->hll && str_val) hll_update(a->hll, str_val);
450
+ }
451
+
452
+ static void accum_free(col_accum *a) {
453
+ free(a->hll);
454
+ free(a->hist);
455
+ free(a->reservoir);
456
+ }
457
+
458
+ /* ---- Process / Flush / Destroy ---- */
459
+
460
+ static int stats_process(tf_step *self, tf_batch *in, tf_batch **out,
461
+ tf_side_channels *side) {
462
+ (void)side;
463
+ stats_state *st = self->state;
464
+ *out = NULL;
465
+
466
+ if (!st->initialized) {
467
+ st->n_cols = in->n_cols;
468
+ st->accums = calloc(in->n_cols, sizeof(col_accum));
469
+ st->col_names = calloc(in->n_cols, sizeof(char *));
470
+ if (!st->accums || !st->col_names) return TF_ERROR;
471
+ for (size_t c = 0; c < in->n_cols; c++) {
472
+ st->col_names[c] = strdup(in->col_names[c]);
473
+ accum_init(&st->accums[c], st);
474
+ }
475
+ st->initialized = 1;
476
+ }
477
+
478
+ for (size_t r = 0; r < in->n_rows; r++) {
479
+ for (size_t c = 0; c < st->n_cols && c < in->n_cols; c++) {
480
+ if (tf_batch_is_null(in, r, c)) continue;
481
+
482
+ double val = 0;
483
+ int is_num = 0;
484
+ char str_buf[64];
485
+ const char *str_val = NULL;
486
+
487
+ switch (in->col_types[c]) {
488
+ case TF_TYPE_INT64: {
489
+ int64_t iv = tf_batch_get_int64(in, r, c);
490
+ val = (double)iv;
491
+ is_num = 1;
492
+ snprintf(str_buf, sizeof(str_buf), "%lld", (long long)iv);
493
+ str_val = str_buf;
494
+ break;
495
+ }
496
+ case TF_TYPE_FLOAT64:
497
+ val = tf_batch_get_float64(in, r, c);
498
+ is_num = 1;
499
+ snprintf(str_buf, sizeof(str_buf), "%.17g", val);
500
+ str_val = str_buf;
501
+ break;
502
+ case TF_TYPE_STRING:
503
+ str_val = tf_batch_get_string(in, r, c);
504
+ break;
505
+ case TF_TYPE_BOOL:
506
+ str_val = tf_batch_get_bool(in, r, c) ? "true" : "false";
507
+ break;
508
+ case TF_TYPE_DATE: {
509
+ int32_t dv = tf_batch_get_date(in, r, c);
510
+ val = (double)dv;
511
+ is_num = 1;
512
+ snprintf(str_buf, sizeof(str_buf), "%d", (int)dv);
513
+ str_val = str_buf;
514
+ break;
515
+ }
516
+ case TF_TYPE_TIMESTAMP: {
517
+ int64_t tv = tf_batch_get_timestamp(in, r, c);
518
+ val = (double)tv;
519
+ is_num = 1;
520
+ snprintf(str_buf, sizeof(str_buf), "%lld", (long long)tv);
521
+ str_val = str_buf;
522
+ break;
523
+ }
524
+ default:
525
+ break;
526
+ }
527
+ accum_update(&st->accums[c], val, str_val, is_num);
528
+ }
529
+ }
530
+
531
+ return TF_OK;
532
+ }
533
+
534
+ static int stats_flush(tf_step *self, tf_batch **out, tf_side_channels *side) {
535
+ (void)side;
536
+ stats_state *st = self->state;
537
+ *out = NULL;
538
+
539
+ if (!st->initialized || st->n_cols == 0) return TF_OK;
540
+
541
+ /* Count output columns */
542
+ size_t n_stat_cols = 1; /* "column" */
543
+ if (st->want_count) n_stat_cols++;
544
+ if (st->want_sum) n_stat_cols++;
545
+ if (st->want_avg) n_stat_cols++;
546
+ if (st->want_min) n_stat_cols++;
547
+ if (st->want_max) n_stat_cols++;
548
+ if (st->want_var) n_stat_cols++;
549
+ if (st->want_stddev) n_stat_cols++;
550
+ if (st->want_median) n_stat_cols++;
551
+ if (st->want_p25) n_stat_cols++;
552
+ if (st->want_p75) n_stat_cols++;
553
+ if (st->want_skewness) n_stat_cols++;
554
+ if (st->want_kurtosis) n_stat_cols++;
555
+ if (st->want_distinct) n_stat_cols++;
556
+ if (st->want_hist) n_stat_cols++;
557
+ if (st->want_sample) n_stat_cols++;
558
+
559
+ tf_batch *ob = tf_batch_create(n_stat_cols, st->n_cols);
560
+ if (!ob) return TF_ERROR;
561
+
562
+ /* Set schema */
563
+ size_t ci = 0;
564
+ tf_batch_set_schema(ob, ci++, "column", TF_TYPE_STRING);
565
+ if (st->want_count) tf_batch_set_schema(ob, ci++, "count", TF_TYPE_INT64);
566
+ if (st->want_sum) tf_batch_set_schema(ob, ci++, "sum", TF_TYPE_FLOAT64);
567
+ if (st->want_avg) tf_batch_set_schema(ob, ci++, "avg", TF_TYPE_FLOAT64);
568
+ if (st->want_min) tf_batch_set_schema(ob, ci++, "min", TF_TYPE_FLOAT64);
569
+ if (st->want_max) tf_batch_set_schema(ob, ci++, "max", TF_TYPE_FLOAT64);
570
+ if (st->want_var) tf_batch_set_schema(ob, ci++, "var", TF_TYPE_FLOAT64);
571
+ if (st->want_stddev) tf_batch_set_schema(ob, ci++, "stddev", TF_TYPE_FLOAT64);
572
+ if (st->want_median) tf_batch_set_schema(ob, ci++, "median", TF_TYPE_FLOAT64);
573
+ if (st->want_p25) tf_batch_set_schema(ob, ci++, "p25", TF_TYPE_FLOAT64);
574
+ if (st->want_p75) tf_batch_set_schema(ob, ci++, "p75", TF_TYPE_FLOAT64);
575
+ if (st->want_skewness) tf_batch_set_schema(ob, ci++, "skewness", TF_TYPE_FLOAT64);
576
+ if (st->want_kurtosis) tf_batch_set_schema(ob, ci++, "kurtosis", TF_TYPE_FLOAT64);
577
+ if (st->want_distinct) tf_batch_set_schema(ob, ci++, "distinct", TF_TYPE_INT64);
578
+ if (st->want_hist) tf_batch_set_schema(ob, ci++, "hist", TF_TYPE_STRING);
579
+ if (st->want_sample) tf_batch_set_schema(ob, ci++, "sample", TF_TYPE_STRING);
580
+
581
+ /* One row per input column */
582
+ for (size_t c = 0; c < st->n_cols; c++) {
583
+ tf_batch_ensure_capacity(ob, c + 1);
584
+ ci = 0;
585
+ tf_batch_set_string(ob, c, ci++, st->col_names[c]);
586
+
587
+ col_accum *a = &st->accums[c];
588
+
589
+ if (st->want_count) {
590
+ tf_batch_set_int64(ob, c, ci++, (int64_t)a->count);
591
+ }
592
+ if (st->want_sum) {
593
+ if (a->has_numeric) tf_batch_set_float64(ob, c, ci, a->sum);
594
+ else tf_batch_set_null(ob, c, ci);
595
+ ci++;
596
+ }
597
+ if (st->want_avg) {
598
+ if (a->has_numeric && a->count > 0)
599
+ tf_batch_set_float64(ob, c, ci, a->sum / (double)a->count);
600
+ else tf_batch_set_null(ob, c, ci);
601
+ ci++;
602
+ }
603
+ if (st->want_min) {
604
+ if (a->has_numeric && a->count > 0) tf_batch_set_float64(ob, c, ci, a->min);
605
+ else tf_batch_set_null(ob, c, ci);
606
+ ci++;
607
+ }
608
+ if (st->want_max) {
609
+ if (a->has_numeric && a->count > 0) tf_batch_set_float64(ob, c, ci, a->max);
610
+ else tf_batch_set_null(ob, c, ci);
611
+ ci++;
612
+ }
613
+ if (st->want_var) {
614
+ if (a->has_numeric && a->count > 1)
615
+ tf_batch_set_float64(ob, c, ci, a->wf_m2 / (double)(a->count - 1));
616
+ else tf_batch_set_null(ob, c, ci);
617
+ ci++;
618
+ }
619
+ if (st->want_stddev) {
620
+ if (a->has_numeric && a->count > 1)
621
+ tf_batch_set_float64(ob, c, ci, sqrt(a->wf_m2 / (double)(a->count - 1)));
622
+ else tf_batch_set_null(ob, c, ci);
623
+ ci++;
624
+ }
625
+ if (st->want_median) {
626
+ if (a->has_numeric && a->count > 0)
627
+ tf_batch_set_float64(ob, c, ci, p2_value(&a->p2_median));
628
+ else tf_batch_set_null(ob, c, ci);
629
+ ci++;
630
+ }
631
+ if (st->want_p25) {
632
+ if (a->has_numeric && a->count > 0)
633
+ tf_batch_set_float64(ob, c, ci, p2_value(&a->p2_p25));
634
+ else tf_batch_set_null(ob, c, ci);
635
+ ci++;
636
+ }
637
+ if (st->want_p75) {
638
+ if (a->has_numeric && a->count > 0)
639
+ tf_batch_set_float64(ob, c, ci, p2_value(&a->p2_p75));
640
+ else tf_batch_set_null(ob, c, ci);
641
+ ci++;
642
+ }
643
+ if (st->want_skewness) {
644
+ if (a->has_numeric && a->count > 2) {
645
+ double m1 = a->mom[0], m2 = a->mom[1], m3 = a->mom[2];
646
+ double vr = m2 - m1 * m1;
647
+ if (vr > 1e-15) {
648
+ double sk = (m3 - 3.0*m1*vr - m1*m1*m1) / pow(vr, 1.5);
649
+ tf_batch_set_float64(ob, c, ci, sk);
650
+ } else {
651
+ tf_batch_set_float64(ob, c, ci, 0.0);
652
+ }
653
+ } else tf_batch_set_null(ob, c, ci);
654
+ ci++;
655
+ }
656
+ if (st->want_kurtosis) {
657
+ if (a->has_numeric && a->count > 3) {
658
+ double m1 = a->mom[0], m2 = a->mom[1];
659
+ double m3 = a->mom[2], m4 = a->mom[3];
660
+ double vr = m2 - m1 * m1;
661
+ if (vr > 1e-15) {
662
+ double kt = (m4 - 4.0*m1*m3 + 6.0*m1*m1*m2
663
+ - 3.0*m1*m1*m1*m1) / (vr*vr) - 3.0;
664
+ tf_batch_set_float64(ob, c, ci, kt);
665
+ } else {
666
+ tf_batch_set_float64(ob, c, ci, 0.0);
667
+ }
668
+ } else tf_batch_set_null(ob, c, ci);
669
+ ci++;
670
+ }
671
+ if (st->want_distinct) {
672
+ if (a->hll && a->count > 0)
673
+ tf_batch_set_int64(ob, c, ci, (int64_t)round(hll_estimate(a->hll)));
674
+ else tf_batch_set_null(ob, c, ci);
675
+ ci++;
676
+ }
677
+ if (st->want_hist) {
678
+ if (a->hist && a->hist->n > 1) {
679
+ /* Format: "lo:hi:c1,c2,...,cN" */
680
+ char buf[1024];
681
+ int pos = snprintf(buf, sizeof(buf), "%.6g:%.6g:",
682
+ a->hist->edges[0], a->hist->edges[HIST_NBINS]);
683
+ for (int b = 0; b < HIST_NBINS && pos < (int)sizeof(buf) - 16; b++) {
684
+ if (b > 0) buf[pos++] = ',';
685
+ pos += snprintf(buf + pos, sizeof(buf) - pos, "%zu", a->hist->counts[b]);
686
+ }
687
+ tf_batch_set_string(ob, c, ci, buf);
688
+ } else {
689
+ tf_batch_set_null(ob, c, ci);
690
+ }
691
+ ci++;
692
+ }
693
+ if (st->want_sample) {
694
+ if (a->reservoir && a->reservoir->n > 0) {
695
+ char buf[512];
696
+ int pos = 0;
697
+ size_t k = a->reservoir->n < RESERVOIR_K ? a->reservoir->n : RESERVOIR_K;
698
+ for (size_t s = 0; s < k && pos < (int)sizeof(buf) - 32; s++) {
699
+ if (s > 0) buf[pos++] = ',';
700
+ pos += snprintf(buf + pos, sizeof(buf) - pos,
701
+ "%.6g", a->reservoir->values[s]);
702
+ }
703
+ tf_batch_set_string(ob, c, ci, buf);
704
+ } else {
705
+ tf_batch_set_null(ob, c, ci);
706
+ }
707
+ ci++;
708
+ }
709
+ ob->n_rows = c + 1;
710
+ }
711
+
712
+ *out = ob;
713
+ return TF_OK;
714
+ }
715
+
716
+ static void stats_destroy(tf_step *self) {
717
+ stats_state *st = self->state;
718
+ if (st) {
719
+ if (st->accums) {
720
+ for (size_t i = 0; i < st->n_cols; i++)
721
+ accum_free(&st->accums[i]);
722
+ free(st->accums);
723
+ }
724
+ if (st->col_names) {
725
+ for (size_t i = 0; i < st->n_cols; i++) free(st->col_names[i]);
726
+ free(st->col_names);
727
+ }
728
+ free(st);
729
+ }
730
+ free(self);
731
+ }
732
+
733
+ tf_step *tf_stats_create(const cJSON *args) {
734
+ stats_state *st = calloc(1, sizeof(stats_state));
735
+ if (!st) return NULL;
736
+
737
+ cJSON *stats_arr = args ? cJSON_GetObjectItemCaseSensitive(args, "stats") : NULL;
738
+ if (stats_arr && cJSON_IsArray(stats_arr)) {
739
+ int n = cJSON_GetArraySize(stats_arr);
740
+ for (int i = 0; i < n; i++) {
741
+ cJSON *item = cJSON_GetArrayItem(stats_arr, i);
742
+ if (!cJSON_IsString(item)) continue;
743
+ const char *s = item->valuestring;
744
+ if (strcmp(s, "count") == 0) st->want_count = 1;
745
+ else if (strcmp(s, "sum") == 0) st->want_sum = 1;
746
+ else if (strcmp(s, "avg") == 0) st->want_avg = 1;
747
+ else if (strcmp(s, "min") == 0) st->want_min = 1;
748
+ else if (strcmp(s, "max") == 0) st->want_max = 1;
749
+ else if (strcmp(s, "var") == 0) st->want_var = 1;
750
+ else if (strcmp(s, "stddev") == 0) st->want_stddev = 1;
751
+ else if (strcmp(s, "median") == 0) st->want_median = 1;
752
+ else if (strcmp(s, "p25") == 0) st->want_p25 = 1;
753
+ else if (strcmp(s, "p75") == 0) st->want_p75 = 1;
754
+ else if (strcmp(s, "skewness") == 0) st->want_skewness = 1;
755
+ else if (strcmp(s, "kurtosis") == 0) st->want_kurtosis = 1;
756
+ else if (strcmp(s, "distinct") == 0) st->want_distinct = 1;
757
+ else if (strcmp(s, "hist") == 0) st->want_hist = 1;
758
+ else if (strcmp(s, "sample") == 0) st->want_sample = 1;
759
+ }
760
+ } else {
761
+ /* Default: basic + variance + median */
762
+ st->want_count = 1;
763
+ st->want_sum = 1;
764
+ st->want_avg = 1;
765
+ st->want_min = 1;
766
+ st->want_max = 1;
767
+ st->want_var = 1;
768
+ st->want_stddev = 1;
769
+ st->want_median = 1;
770
+ }
771
+
772
+ tf_step *step = malloc(sizeof(tf_step));
773
+ if (!step) { free(st); return NULL; }
774
+ step->process = stats_process;
775
+ step->flush = stats_flush;
776
+ step->destroy = stats_destroy;
777
+ step->state = st;
778
+ return step;
779
+ }