tranfi 0.1.2 → 0.2.1

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 (132) hide show
  1. package/LICENSE +177 -0
  2. package/NOTICE +8 -0
  3. package/README.md +443 -51
  4. package/app/assets/{index-pDFMluyz.js → index-BIAIKnrp.js} +1 -1
  5. package/app/index.html +1 -1
  6. package/binding.gyp +55 -3
  7. package/csrc/arena.c +7 -5
  8. package/csrc/batch.c +818 -71
  9. package/csrc/buffer.c +84 -8
  10. package/csrc/cJSON.c +262 -19
  11. package/csrc/cJSON.h +17 -1
  12. package/csrc/codec_csv.c +1074 -181
  13. package/csrc/codec_jsonl.c +830 -118
  14. package/csrc/codec_table.c +108 -78
  15. package/csrc/codec_text.c +286 -68
  16. package/csrc/compiler.c +31 -3
  17. package/csrc/config.h +21 -0
  18. package/csrc/dsl.c +4722 -485
  19. package/csrc/expr.c +363 -55
  20. package/csrc/expr.h +2 -0
  21. package/csrc/internal.h +316 -27
  22. package/csrc/ir.c +65 -18
  23. package/csrc/ir.h +41 -0
  24. package/csrc/ir_schema.c +20 -5
  25. package/csrc/ir_serialize.c +68 -6
  26. package/csrc/ir_sql.c +796 -185
  27. package/csrc/ir_validate.c +462 -6
  28. package/csrc/json_path.c +210 -0
  29. package/csrc/main.c +879 -30
  30. package/csrc/memory_estimate.c +477 -0
  31. package/csrc/op_acf.c +171 -21
  32. package/csrc/op_across.c +477 -0
  33. package/csrc/op_anomaly.c +167 -32
  34. package/csrc/op_assert.c +761 -0
  35. package/csrc/op_bin.c +168 -29
  36. package/csrc/op_cast.c +383 -55
  37. package/csrc/op_clip.c +30 -19
  38. package/csrc/op_date_trunc.c +208 -34
  39. package/csrc/op_datetime.c +259 -77
  40. package/csrc/op_derive.c +65 -97
  41. package/csrc/op_diff.c +146 -30
  42. package/csrc/op_ewma.c +149 -30
  43. package/csrc/op_explode.c +124 -26
  44. package/csrc/op_fill_down.c +125 -53
  45. package/csrc/op_fill_null.c +176 -31
  46. package/csrc/op_filter.c +89 -40
  47. package/csrc/op_frequency.c +571 -43
  48. package/csrc/op_grep.c +36 -18
  49. package/csrc/op_group_agg.c +1790 -119
  50. package/csrc/op_hash.c +48 -15
  51. package/csrc/op_head.c +21 -86
  52. package/csrc/op_interpolate.c +268 -62
  53. package/csrc/op_join.c +2700 -182
  54. package/csrc/op_json_extract.c +227 -0
  55. package/csrc/op_json_filter.c +384 -0
  56. package/csrc/op_json_flatten.c +293 -0
  57. package/csrc/op_json_schema.c +503 -0
  58. package/csrc/op_label_encode.c +328 -53
  59. package/csrc/op_lag.c +181 -0
  60. package/csrc/op_lead.c +141 -89
  61. package/csrc/op_normalize.c +363 -79
  62. package/csrc/op_onehot.c +345 -73
  63. package/csrc/op_pivot.c +1546 -162
  64. package/csrc/op_quarantine.c +189 -0
  65. package/csrc/op_registry.c +2062 -166
  66. package/csrc/op_rename.c +41 -50
  67. package/csrc/op_replace.c +270 -118
  68. package/csrc/op_rleid.c +297 -0
  69. package/csrc/op_rowid.c +559 -0
  70. package/csrc/op_sample.c +80 -23
  71. package/csrc/op_schema.c +1341 -0
  72. package/csrc/op_schema_infer.c +252 -0
  73. package/csrc/op_select.c +265 -65
  74. package/csrc/op_set.c +3449 -0
  75. package/csrc/op_skip.c +30 -87
  76. package/csrc/op_sort.c +670 -124
  77. package/csrc/op_source_name.c +120 -0
  78. package/csrc/op_split.c +65 -28
  79. package/csrc/op_split_data.c +41 -9
  80. package/csrc/op_stack.c +178 -222
  81. package/csrc/op_stats.c +206 -110
  82. package/csrc/op_step.c +217 -55
  83. package/csrc/op_tail.c +21 -12
  84. package/csrc/op_tee.c +338 -0
  85. package/csrc/op_top.c +260 -53
  86. package/csrc/op_trim.c +48 -19
  87. package/csrc/op_unique.c +1193 -150
  88. package/csrc/op_unpivot.c +100 -66
  89. package/csrc/op_validate.c +601 -24
  90. package/csrc/op_window.c +492 -51
  91. package/csrc/path_policy.c +85 -0
  92. package/csrc/pipeline.c +872 -99
  93. package/csrc/recipes.c +3 -1
  94. package/csrc/report.c +73 -30
  95. package/csrc/selector.c +1097 -0
  96. package/csrc/size_utils.c +352 -0
  97. package/csrc/spill.c +317 -0
  98. package/csrc/spill.h +21 -0
  99. package/csrc/tranfi.h +169 -1
  100. package/csrc/transform.h +209 -0
  101. package/csrc/transform_api.c +2237 -0
  102. package/csrc/transform_categorical.c +923 -0
  103. package/csrc/transform_internal.h +472 -0
  104. package/csrc/transform_json.c +3812 -0
  105. package/csrc/transform_numeric.c +1966 -0
  106. package/csrc/transform_sha256.c +154 -0
  107. package/csrc/transform_wasm.h +162 -0
  108. package/csrc/transform_wasm_api.c +1373 -0
  109. package/csrc/wasm_api.c +70 -9
  110. package/napi_api.c +219 -11
  111. package/napi_transform.c +1648 -0
  112. package/napi_transform.h +8 -0
  113. package/package.json +27 -11
  114. package/scripts/install-native.js +76 -0
  115. package/scripts/prepack.js +64 -0
  116. package/scripts/sync-csrc.js +23 -0
  117. package/src/cli.js +81 -41
  118. package/src/engines/duckdb.js +45 -12
  119. package/src/index.js +661 -42
  120. package/src/memory_policy.js +411 -0
  121. package/src/native.js +1 -5
  122. package/src/pipeline.js +454 -31
  123. package/src/recipe_json.js +80 -0
  124. package/src/server.js +10 -8
  125. package/src/transform.js +403 -0
  126. package/src/transform_error.js +10 -0
  127. package/src/wasm.js +6 -4
  128. package/wasm/index.js +498 -10
  129. package/wasm/tranfi_core.js +0 -0
  130. package/wasm/transform.js +1156 -0
  131. package/wasm/worker.js +786 -0
  132. package/csrc/plan.c +0 -206
package/csrc/op_stats.c CHANGED
@@ -1,10 +1,14 @@
1
1
  /*
2
2
  * op_stats.c — Streaming aggregates with online algorithms.
3
3
  *
4
- * Config: {"stats": ["count","sum","avg","min","max","var","stddev",
5
- * "median","p25","p75","skewness","kurtosis",
6
- * "distinct","hist","sample"]}
4
+ * Config: {"stats": ["count","missing","complete_rate","sum","avg",
5
+ * "min","max","var","stddev","median","p25",
6
+ * "p75","skewness","kurtosis","distinct",
7
+ * "hist","sample"]}
7
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.
8
12
  *
9
13
  * Algorithms ported from OnlineStats.jl (MIT):
10
14
  * - Welford's online variance (var, stddev)
@@ -182,8 +186,9 @@ static void hll_update(hll_state *h, const char *val) {
182
186
  uint32_t x = hll_hash(val);
183
187
  uint32_t idx = x & (HLL_M - 1);
184
188
  uint32_t w = x >> HLL_P;
185
- int rho = hll_clz32(w) + 1;
186
- if (rho > 32 - HLL_P) rho = 32 - 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;
187
192
  if ((uint8_t)rho > h->M[idx]) h->M[idx] = (uint8_t)rho;
188
193
  }
189
194
 
@@ -345,6 +350,7 @@ static void reservoir_update(reservoir_state *r, double y) {
345
350
 
346
351
  typedef struct {
347
352
  /* Basic stats */
353
+ size_t total;
348
354
  size_t count;
349
355
  double sum;
350
356
  double min;
@@ -381,7 +387,8 @@ typedef struct {
381
387
  size_t n_cols;
382
388
  int initialized;
383
389
  /* Which stats to output */
384
- int want_count, want_sum, want_avg, want_min, want_max;
390
+ int want_count, want_missing, want_complete_rate;
391
+ int want_sum, want_avg, want_min, want_max;
385
392
  int want_var, want_stddev;
386
393
  int want_median, want_p25, want_p75;
387
394
  int want_skewness, want_kurtosis;
@@ -390,7 +397,18 @@ typedef struct {
390
397
  int want_sample;
391
398
  } stats_state;
392
399
 
393
- static void accum_init(col_accum *a, const stats_state *st) {
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) {
394
412
  a->min = DBL_MAX;
395
413
  a->max = -DBL_MAX;
396
414
  p2_init(&a->p2_median, 0.5);
@@ -398,17 +416,21 @@ static void accum_init(col_accum *a, const stats_state *st) {
398
416
  p2_init(&a->p2_p75, 0.75);
399
417
 
400
418
  if (st->want_distinct) {
401
- a->hll = calloc(1, sizeof(hll_state));
402
- if (a->hll) hll_init(a->hll);
419
+ a->hll = tf_callocarray_checked(1, sizeof(hll_state));
420
+ if (!a->hll) return TF_ERROR;
421
+ hll_init(a->hll);
403
422
  }
404
423
  if (st->want_hist) {
405
- a->hist = calloc(1, sizeof(hist_state));
406
- if (a->hist) hist_init(a->hist);
424
+ a->hist = tf_callocarray_checked(1, sizeof(hist_state));
425
+ if (!a->hist) return TF_ERROR;
426
+ hist_init(a->hist);
407
427
  }
408
428
  if (st->want_sample) {
409
- a->reservoir = calloc(1, sizeof(reservoir_state));
410
- if (a->reservoir) reservoir_init(a->reservoir);
429
+ a->reservoir = tf_callocarray_checked(1, sizeof(reservoir_state));
430
+ if (!a->reservoir) return TF_ERROR;
431
+ reservoir_init(a->reservoir);
411
432
  }
433
+ return TF_OK;
412
434
  }
413
435
 
414
436
  static void accum_update(col_accum *a, double val, const char *str_val,
@@ -455,6 +477,23 @@ static void accum_free(col_accum *a) {
455
477
  free(a->reservoir);
456
478
  }
457
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
+
458
497
  /* ---- Process / Flush / Destroy ---- */
459
498
 
460
499
  static int stats_process(tf_step *self, tf_batch *in, tf_batch **out,
@@ -465,18 +504,26 @@ static int stats_process(tf_step *self, tf_batch *in, tf_batch **out,
465
504
 
466
505
  if (!st->initialized) {
467
506
  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;
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
+ }
471
513
  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);
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
+ }
474
519
  }
475
520
  st->initialized = 1;
476
521
  }
477
522
 
478
523
  for (size_t r = 0; r < in->n_rows; r++) {
479
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++;
480
527
  if (tf_batch_is_null(in, r, c)) continue;
481
528
 
482
529
  double val = 0;
@@ -524,7 +571,7 @@ static int stats_process(tf_step *self, tf_batch *in, tf_batch **out,
524
571
  default:
525
572
  break;
526
573
  }
527
- accum_update(&st->accums[c], val, str_val, is_num);
574
+ accum_update(a, val, str_val, is_num);
528
575
  }
529
576
  }
530
577
 
@@ -540,8 +587,10 @@ static int stats_flush(tf_step *self, tf_batch **out, tf_side_channels *side) {
540
587
 
541
588
  /* Count output columns */
542
589
  size_t n_stat_cols = 1; /* "column" */
543
- if (st->want_count) n_stat_cols++;
544
- if (st->want_sum) n_stat_cols++;
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++;
545
594
  if (st->want_avg) n_stat_cols++;
546
595
  if (st->want_min) n_stat_cols++;
547
596
  if (st->want_max) n_stat_cols++;
@@ -558,86 +607,102 @@ static int stats_flush(tf_step *self, tf_batch **out, tf_side_channels *side) {
558
607
 
559
608
  tf_batch *ob = tf_batch_create(n_stat_cols, st->n_cols);
560
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)
561
612
 
562
613
  /* Set schema */
563
614
  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);
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));
580
633
 
581
634
  /* One row per input column */
582
635
  for (size_t c = 0; c < st->n_cols; c++) {
583
- tf_batch_ensure_capacity(ob, c + 1);
636
+ STATS_WRITE(tf_batch_ensure_capacity(ob, c + 1));
584
637
  ci = 0;
585
- tf_batch_set_string(ob, c, ci++, st->col_names[c]);
638
+ STATS_WRITE(tf_batch_set_string(ob, c, ci++, st->col_names[c]));
586
639
 
587
640
  col_accum *a = &st->accums[c];
588
641
 
589
642
  if (st->want_count) {
590
- tf_batch_set_int64(ob, c, ci++, (int64_t)a->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++;
591
656
  }
592
657
  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);
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));
595
660
  ci++;
596
661
  }
597
662
  if (st->want_avg) {
598
663
  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);
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));
601
666
  ci++;
602
667
  }
603
668
  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);
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));
606
671
  ci++;
607
672
  }
608
673
  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);
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));
611
676
  ci++;
612
677
  }
613
678
  if (st->want_var) {
614
679
  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);
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));
617
682
  ci++;
618
683
  }
619
684
  if (st->want_stddev) {
620
685
  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);
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));
623
688
  ci++;
624
689
  }
625
690
  if (st->want_median) {
626
691
  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);
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));
629
694
  ci++;
630
695
  }
631
696
  if (st->want_p25) {
632
697
  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);
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));
635
700
  ci++;
636
701
  }
637
702
  if (st->want_p75) {
638
703
  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);
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));
641
706
  ci++;
642
707
  }
643
708
  if (st->want_skewness) {
@@ -646,11 +711,11 @@ static int stats_flush(tf_step *self, tf_batch **out, tf_side_channels *side) {
646
711
  double vr = m2 - m1 * m1;
647
712
  if (vr > 1e-15) {
648
713
  double sk = (m3 - 3.0*m1*vr - m1*m1*m1) / pow(vr, 1.5);
649
- tf_batch_set_float64(ob, c, ci, sk);
714
+ STATS_WRITE(tf_batch_set_float64(ob, c, ci, sk));
650
715
  } else {
651
- tf_batch_set_float64(ob, c, ci, 0.0);
716
+ STATS_WRITE(tf_batch_set_float64(ob, c, ci, 0.0));
652
717
  }
653
- } else tf_batch_set_null(ob, c, ci);
718
+ } else STATS_WRITE(tf_batch_set_null(ob, c, ci));
654
719
  ci++;
655
720
  }
656
721
  if (st->want_kurtosis) {
@@ -661,17 +726,17 @@ static int stats_flush(tf_step *self, tf_batch **out, tf_side_channels *side) {
661
726
  if (vr > 1e-15) {
662
727
  double kt = (m4 - 4.0*m1*m3 + 6.0*m1*m1*m2
663
728
  - 3.0*m1*m1*m1*m1) / (vr*vr) - 3.0;
664
- tf_batch_set_float64(ob, c, ci, kt);
729
+ STATS_WRITE(tf_batch_set_float64(ob, c, ci, kt));
665
730
  } else {
666
- tf_batch_set_float64(ob, c, ci, 0.0);
731
+ STATS_WRITE(tf_batch_set_float64(ob, c, ci, 0.0));
667
732
  }
668
- } else tf_batch_set_null(ob, c, ci);
733
+ } else STATS_WRITE(tf_batch_set_null(ob, c, ci));
669
734
  ci++;
670
735
  }
671
736
  if (st->want_distinct) {
672
737
  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);
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));
675
740
  ci++;
676
741
  }
677
742
  if (st->want_hist) {
@@ -684,9 +749,9 @@ static int stats_flush(tf_step *self, tf_batch **out, tf_side_channels *side) {
684
749
  if (b > 0) buf[pos++] = ',';
685
750
  pos += snprintf(buf + pos, sizeof(buf) - pos, "%zu", a->hist->counts[b]);
686
751
  }
687
- tf_batch_set_string(ob, c, ci, buf);
752
+ STATS_WRITE(tf_batch_set_string(ob, c, ci, buf));
688
753
  } else {
689
- tf_batch_set_null(ob, c, ci);
754
+ STATS_WRITE(tf_batch_set_null(ob, c, ci));
690
755
  }
691
756
  ci++;
692
757
  }
@@ -700,38 +765,83 @@ static int stats_flush(tf_step *self, tf_batch **out, tf_side_channels *side) {
700
765
  pos += snprintf(buf + pos, sizeof(buf) - pos,
701
766
  "%.6g", a->reservoir->values[s]);
702
767
  }
703
- tf_batch_set_string(ob, c, ci, buf);
768
+ STATS_WRITE(tf_batch_set_string(ob, c, ci, buf));
704
769
  } else {
705
- tf_batch_set_null(ob, c, ci);
770
+ STATS_WRITE(tf_batch_set_null(ob, c, ci));
706
771
  }
707
772
  ci++;
708
773
  }
709
- ob->n_rows = c + 1;
774
+ STATS_WRITE(tf_batch_expose_row(ob, c));
710
775
  }
711
776
 
712
777
  *out = ob;
713
- return TF_OK;
778
+ ob = NULL;
779
+ rc = TF_OK;
780
+
781
+ done:
782
+ if (ob) tf_batch_free(ob);
783
+ #undef STATS_WRITE
784
+ return rc;
714
785
  }
715
786
 
716
787
  static void stats_destroy(tf_step *self) {
717
788
  stats_state *st = self->state;
718
789
  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
- }
790
+ stats_clear_accumulation(st);
728
791
  free(st);
729
792
  }
730
793
  free(self);
731
794
  }
732
795
 
733
- tf_step *tf_stats_create(const cJSON *args) {
734
- stats_state *st = calloc(1, sizeof(stats_state));
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));
735
845
  if (!st) return NULL;
736
846
 
737
847
  cJSON *stats_arr = args ? cJSON_GetObjectItemCaseSensitive(args, "stats") : NULL;
@@ -739,37 +849,15 @@ tf_step *tf_stats_create(const cJSON *args) {
739
849
  int n = cJSON_GetArraySize(stats_arr);
740
850
  for (int i = 0; i < n; i++) {
741
851
  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;
852
+ if (cJSON_IsString(item)) stats_enable_name(st, item->valuestring);
759
853
  }
854
+ } else if (scan_defaults) {
855
+ scan_enable_default(st);
760
856
  } 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;
857
+ stats_enable_default(st);
770
858
  }
771
859
 
772
- tf_step *step = malloc(sizeof(tf_step));
860
+ tf_step *step = tf_callocarray_checked(1, sizeof(tf_step));
773
861
  if (!step) { free(st); return NULL; }
774
862
  step->process = stats_process;
775
863
  step->flush = stats_flush;
@@ -777,3 +865,11 @@ tf_step *tf_stats_create(const cJSON *args) {
777
865
  step->state = st;
778
866
  return step;
779
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
+ }