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