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
@@ -15,25 +15,89 @@
15
15
  typedef struct {
16
16
  char **keys;
17
17
  size_t *counts;
18
+ size_t *errors;
18
19
  size_t count;
19
20
  size_t cap;
21
+ size_t key_bytes;
20
22
  } freq_map;
21
23
 
22
24
  typedef struct {
23
25
  char **cols;
24
26
  size_t n_cols;
27
+ size_t max_values; /* 0 = unlimited */
28
+ size_t max_state_bytes; /* 0 = unlimited */
29
+ int approximate;
30
+ int overflow_other;
31
+ char *other_label;
32
+ size_t other_count;
33
+ size_t approx_replacements;
34
+ size_t row_index;
35
+ size_t audit_limit;
36
+ size_t audit_emitted;
37
+ int audit;
38
+ tf_audit_options audit_opts;
25
39
  freq_map map;
26
40
  } frequency_state;
27
41
 
42
+ static size_t frequency_retained_state_bytes(const frequency_state *st);
43
+
44
+ static int frequency_write_error(tf_side_channels *side, const char *msg) {
45
+ return tf_side_write_error(side, msg);
46
+ }
47
+
48
+ static int frequency_limit_error(const frequency_state *st, tf_side_channels *side) {
49
+ char msg[160];
50
+ snprintf(msg, sizeof(msg),
51
+ "frequency: max_values=%zu exceeded while tracking exact counts",
52
+ st->max_values);
53
+ return frequency_write_error(side, msg);
54
+ }
55
+
56
+ static int frequency_check_state_bytes(frequency_state *st, tf_side_channels *side) {
57
+ if (!st || st->max_state_bytes == 0) return 0;
58
+ size_t retained = frequency_retained_state_bytes(st);
59
+ if (retained <= st->max_state_bytes) return 0;
60
+ char msg[208];
61
+ snprintf(msg, sizeof(msg),
62
+ "frequency: max_state_bytes=%zu exceeded while tracking %s counts (%zu bytes retained)",
63
+ st->max_state_bytes, st->approximate ? "approximate" : "exact", retained);
64
+ if (frequency_write_error(side, msg) != TF_OK) return -1;
65
+ return -1;
66
+ }
67
+
68
+ static int freq_buf_ensure(char **buf, size_t *cap, size_t len, size_t extra) {
69
+ size_t need = 0;
70
+ if (!buf || !*buf || !cap ||
71
+ tf_size_add(len, extra, &need) != TF_OK ||
72
+ tf_size_add(need, 1, &need) != TF_OK) {
73
+ return TF_ERROR;
74
+ }
75
+ if (need <= *cap) return TF_OK;
76
+
77
+ size_t new_cap = 0;
78
+ if (tf_size_grow_pow2(*cap, need, 256, &new_cap) != TF_OK) return TF_ERROR;
79
+ char *tmp = tf_reallocarray_checked(*buf, new_cap, sizeof(char));
80
+ if (!tmp) return TF_ERROR;
81
+ *buf = tmp;
82
+ *cap = new_cap;
83
+ return TF_OK;
84
+ }
85
+
28
86
  static char *build_freq_key(const tf_batch *b, size_t row,
29
87
  int *col_indices, size_t n_keys) {
30
88
  size_t buf_cap = 256;
31
- char *buf = malloc(buf_cap);
89
+ char *buf = tf_mallocarray_checked(buf_cap, sizeof(char));
32
90
  if (!buf) return NULL;
33
91
  size_t buf_len = 0;
34
92
  for (size_t k = 0; k < n_keys; k++) {
35
93
  int c = col_indices[k];
36
- if (k > 0) { if (buf_len + 1 >= buf_cap) { buf_cap *= 2; buf = realloc(buf, buf_cap); } buf[buf_len++] = '\x01'; }
94
+ if (k > 0) {
95
+ if (freq_buf_ensure(&buf, &buf_cap, buf_len, 1) != TF_OK) {
96
+ free(buf);
97
+ return NULL;
98
+ }
99
+ buf[buf_len++] = '\x01';
100
+ }
37
101
  char val_buf[64];
38
102
  const char *val;
39
103
  size_t val_len;
@@ -46,7 +110,11 @@ static char *build_freq_key(const tf_batch *b, size_t row,
46
110
  val_len = snprintf(val_buf, sizeof(val_buf), "%lld", (long long)tf_batch_get_int64(b, row, c));
47
111
  val = val_buf; break;
48
112
  case TF_TYPE_FLOAT64:
49
- val_len = snprintf(val_buf, sizeof(val_buf), "%g", tf_batch_get_float64(b, row, c));
113
+ if (tf_format_float64(val_buf, sizeof(val_buf), tf_batch_get_float64(b, row, c)) != TF_OK) {
114
+ free(buf);
115
+ return NULL;
116
+ }
117
+ val_len = strlen(val_buf);
50
118
  val = val_buf; break;
51
119
  case TF_TYPE_BOOL:
52
120
  val = tf_batch_get_bool(b, row, c) ? "T" : "F"; val_len = 1; break;
@@ -59,7 +127,10 @@ static char *build_freq_key(const tf_batch *b, size_t row,
59
127
  default: val = "\\N"; val_len = 2; break;
60
128
  }
61
129
  }
62
- while (buf_len + val_len + 2 >= buf_cap) { buf_cap *= 2; buf = realloc(buf, buf_cap); }
130
+ if (freq_buf_ensure(&buf, &buf_cap, buf_len, val_len) != TF_OK) {
131
+ free(buf);
132
+ return NULL;
133
+ }
63
134
  memcpy(buf + buf_len, val, val_len);
64
135
  buf_len += val_len;
65
136
  }
@@ -67,25 +138,244 @@ static char *build_freq_key(const tf_batch *b, size_t row,
67
138
  return buf;
68
139
  }
69
140
 
70
- static int freq_add(freq_map *map, const char *key) {
141
+
142
+
143
+ static const char *frequency_format_key_for_audit(frequency_state *st, const char *key,
144
+ const tf_batch *b, size_t row,
145
+ char *buf, size_t buf_size) {
146
+ const char *src = key ? key : "";
147
+ if (!st || !buf || buf_size == 0) return src;
148
+ int redact = 0;
149
+ int hash = 0;
150
+ if (b && row < b->n_rows) {
151
+ if (st->n_cols > 0) {
152
+ for (size_t i = 0; i < st->n_cols; i++) {
153
+ int ci = tf_batch_col_index(b, st->cols[i]);
154
+ if (ci < 0) continue;
155
+ const char *name = b->col_names[ci] ? b->col_names[ci] : "";
156
+ if (tf_audit_column_is_redacted(&st->audit_opts, name)) redact = 1;
157
+ if (tf_audit_column_is_hashed(&st->audit_opts, name)) hash = 1;
158
+ }
159
+ } else {
160
+ for (size_t c = 0; c < b->n_cols; c++) {
161
+ const char *name = b->col_names[c] ? b->col_names[c] : "";
162
+ if (tf_audit_column_is_redacted(&st->audit_opts, name)) redact = 1;
163
+ if (tf_audit_column_is_hashed(&st->audit_opts, name)) hash = 1;
164
+ }
165
+ }
166
+ }
167
+ if (redact) {
168
+ snprintf(buf, buf_size, "[REDACTED]");
169
+ return buf;
170
+ }
171
+ if (hash) {
172
+ if (tf_audit_hash_string(src, buf, buf_size) == TF_OK) return buf;
173
+ return "[HASHED]";
174
+ }
175
+ if (st->audit_opts.max_cell_bytes > 0 && strlen(src) > st->audit_opts.max_cell_bytes) {
176
+ size_t n = st->audit_opts.max_cell_bytes;
177
+ if (n + 4 > buf_size) n = buf_size > 4 ? buf_size - 4 : 0;
178
+ if (n > 0) memcpy(buf, src, n);
179
+ snprintf(buf + n, buf_size - n, "...");
180
+ return buf;
181
+ }
182
+ return src;
183
+ }
184
+
185
+ static int emit_frequency_overflow_audit(frequency_state *st, const char *key,
186
+ const tf_batch *b, size_t row,
187
+ size_t row_no, tf_side_channels *side) {
188
+ if (!st->audit || st->audit_emitted >= st->audit_limit || !side || !side->stats) return TF_OK;
189
+ cJSON *obj = cJSON_CreateObject();
190
+ if (!obj) return TF_ERROR;
191
+ int rc = TF_ERROR;
192
+ if (tf_json_add_string(obj, "type", "audit") != TF_OK ||
193
+ tf_json_add_string(obj, "op", "frequency") != TF_OK ||
194
+ tf_json_add_string(obj, "event", "category_overflow") != TF_OK ||
195
+ tf_json_add_string(obj, "reason", "max_values_overflow") != TF_OK ||
196
+ tf_json_add_string(obj, "channel", "audit") != TF_OK ||
197
+ tf_json_add_string(obj, "action", "other") != TF_OK) {
198
+ goto done;
199
+ }
200
+ char key_buf[256];
201
+ const char *safe_key = frequency_format_key_for_audit(st, key, b, row, key_buf, sizeof(key_buf));
202
+ if (tf_json_add_string(obj, "value", safe_key ? safe_key : "") != TF_OK ||
203
+ tf_json_add_string(obj, "bucket", st->other_label ? st->other_label : "__other__") != TF_OK ||
204
+ tf_json_add_number(obj, "row", (double)row_no) != TF_OK ||
205
+ tf_json_add_number(obj, "max_values", (double)st->max_values) != TF_OK ||
206
+ tf_json_add_number(obj, "tracked_values", (double)st->map.count) != TF_OK) {
207
+ goto done;
208
+ }
209
+ cJSON *row_obj = tf_audit_row_to_json(b, row, &st->audit_opts);
210
+ if (row_obj) {
211
+ if (tf_json_add_item(obj, "data", row_obj) != TF_OK) goto done;
212
+ } else if (st->audit_opts.include_row) {
213
+ goto done;
214
+ }
215
+ rc = tf_buffer_write_json_line(side->stats, obj);
216
+ done:
217
+ cJSON_Delete(obj);
218
+ if (rc == TF_OK) st->audit_emitted++;
219
+ return rc;
220
+ }
221
+
222
+ static int freq_find(const freq_map *map, const char *key, size_t *idx_out) {
71
223
  for (size_t i = 0; i < map->count; i++) {
72
- if (strcmp(map->keys[i], key) == 0) { map->counts[i]++; return 0; }
224
+ if (strcmp(map->keys[i], key) == 0) {
225
+ if (idx_out) *idx_out = i;
226
+ return 1;
227
+ }
228
+ }
229
+ return 0;
230
+ }
231
+
232
+ static int freq_map_reserve(freq_map *map, size_t need) {
233
+ if (need <= map->cap) return 0;
234
+ size_t new_cap = 0;
235
+ if (tf_size_grow_pow2(map->cap, need, 64, &new_cap) != TF_OK) return -1;
236
+
237
+ char **new_keys = tf_callocarray_checked(new_cap, sizeof(char *));
238
+ size_t *new_counts = tf_callocarray_checked(new_cap, sizeof(size_t));
239
+ size_t *new_errors = tf_callocarray_checked(new_cap, sizeof(size_t));
240
+ if (!new_keys || !new_counts || !new_errors) {
241
+ free(new_keys);
242
+ free(new_counts);
243
+ free(new_errors);
244
+ return -1;
73
245
  }
74
- if (map->count >= map->cap) {
75
- size_t new_cap = map->cap ? map->cap * 2 : 64;
76
- map->keys = realloc(map->keys, new_cap * sizeof(char *));
77
- map->counts = realloc(map->counts, new_cap * sizeof(size_t));
78
- map->cap = new_cap;
246
+ if (map->count > 0) {
247
+ size_t key_bytes = 0;
248
+ size_t count_bytes = 0;
249
+ if (tf_size_mul(map->count, sizeof(char *), &key_bytes) != TF_OK ||
250
+ tf_size_mul(map->count, sizeof(size_t), &count_bytes) != TF_OK) {
251
+ free(new_keys);
252
+ free(new_counts);
253
+ free(new_errors);
254
+ return -1;
255
+ }
256
+ memcpy(new_keys, map->keys, key_bytes);
257
+ memcpy(new_counts, map->counts, count_bytes);
258
+ memcpy(new_errors, map->errors, count_bytes);
259
+ }
260
+ free(map->keys);
261
+ free(map->counts);
262
+ free(map->errors);
263
+ map->keys = new_keys;
264
+ map->counts = new_counts;
265
+ map->errors = new_errors;
266
+ map->cap = new_cap;
267
+ return 0;
268
+ }
269
+
270
+ static int freq_map_add_entry(freq_map *map, const char *key,
271
+ size_t count, size_t error) {
272
+ size_t need = 0;
273
+ if (tf_size_add(map->count, 1, &need) != TF_OK ||
274
+ freq_map_reserve(map, need) != 0) {
275
+ return -1;
276
+ }
277
+
278
+ char *dup = strdup(key);
279
+ if (!dup) return -1;
280
+ size_t key_bytes_delta = 0;
281
+ size_t new_key_bytes = 0;
282
+ if (tf_size_add(strlen(dup), 1, &key_bytes_delta) != TF_OK ||
283
+ tf_size_add(map->key_bytes, key_bytes_delta, &new_key_bytes) != TF_OK) {
284
+ free(dup);
285
+ return -1;
79
286
  }
80
- map->keys[map->count] = strdup(key);
81
- map->counts[map->count] = 1;
287
+ map->keys[map->count] = dup;
288
+ map->key_bytes = new_key_bytes;
289
+ map->counts[map->count] = count;
290
+ map->errors[map->count] = error;
82
291
  map->count++;
83
292
  return 0;
84
293
  }
85
294
 
295
+ static size_t freq_approx_victim(const freq_map *map) {
296
+ size_t victim = 0;
297
+ for (size_t i = 1; i < map->count; i++) {
298
+ if (map->counts[i] < map->counts[victim] ||
299
+ (map->counts[i] == map->counts[victim] &&
300
+ (map->errors[i] < map->errors[victim] ||
301
+ (map->errors[i] == map->errors[victim] &&
302
+ strcmp(map->keys[i], map->keys[victim]) < 0)))) {
303
+ victim = i;
304
+ }
305
+ }
306
+ return victim;
307
+ }
308
+
309
+ static int freq_approx_replace(frequency_state *st, const char *key) {
310
+ freq_map *map = &st->map;
311
+ size_t victim = freq_approx_victim(map);
312
+ size_t old_count = map->counts[victim];
313
+ size_t new_count = 0;
314
+ size_t replacements = 0;
315
+ if (tf_size_add(old_count, 1, &new_count) != TF_OK ||
316
+ tf_size_add(st->approx_replacements, 1, &replacements) != TF_OK) {
317
+ return -1;
318
+ }
319
+
320
+ char *dup = strdup(key);
321
+ if (!dup) return -1;
322
+ size_t old_key_bytes = 0;
323
+ size_t new_key_bytes_delta = 0;
324
+ size_t new_key_bytes = 0;
325
+ if (tf_size_add(strlen(map->keys[victim]), 1, &old_key_bytes) != TF_OK ||
326
+ tf_size_add(strlen(dup), 1, &new_key_bytes_delta) != TF_OK ||
327
+ map->key_bytes < old_key_bytes ||
328
+ tf_size_add(map->key_bytes - old_key_bytes, new_key_bytes_delta, &new_key_bytes) != TF_OK) {
329
+ free(dup);
330
+ return -1;
331
+ }
332
+
333
+ free(map->keys[victim]);
334
+ map->keys[victim] = dup;
335
+ map->key_bytes = new_key_bytes;
336
+ map->counts[victim] = new_count;
337
+ map->errors[victim] = old_count;
338
+ st->approx_replacements = replacements;
339
+ return 0;
340
+ }
341
+
342
+ static int freq_add(frequency_state *st, const char *key, const tf_batch *b, size_t row, size_t row_no, tf_side_channels *side) {
343
+ freq_map *map = &st->map;
344
+ if (st->overflow_other && st->other_label && strcmp(key, st->other_label) == 0) {
345
+ if (tf_size_add(st->other_count, 1, &st->other_count) != TF_OK) return -1;
346
+ return 0;
347
+ }
348
+ size_t idx = 0;
349
+ if (freq_find(map, key, &idx)) {
350
+ if (tf_size_add(map->counts[idx], 1, &map->counts[idx]) != TF_OK) return -1;
351
+ return 0;
352
+ }
353
+ if (st->approximate) {
354
+ if (st->max_values == 0) return -1;
355
+ if (map->count < st->max_values) {
356
+ if (freq_map_add_entry(map, key, 1, 0) != 0) return -1;
357
+ } else {
358
+ if (freq_approx_replace(st, key) != 0) return -1;
359
+ }
360
+ if (frequency_check_state_bytes(st, side) != 0) return -1;
361
+ return 0;
362
+ }
363
+ if (st->max_values > 0 && map->count >= st->max_values) {
364
+ if (st->overflow_other) {
365
+ if (tf_size_add(st->other_count, 1, &st->other_count) != TF_OK) return -1;
366
+ if (emit_frequency_overflow_audit(st, key, b, row, row_no, side) != TF_OK) return -1;
367
+ return 0;
368
+ }
369
+ if (frequency_limit_error(st, side) != TF_OK) return -1;
370
+ return -1;
371
+ }
372
+ if (freq_map_add_entry(map, key, 1, 0) != 0) return -1;
373
+ if (frequency_check_state_bytes(st, side) != 0) return -1;
374
+ return 0;
375
+ }
376
+
86
377
  static int frequency_process(tf_step *self, tf_batch *in, tf_batch **out,
87
378
  tf_side_channels *side) {
88
- (void)side;
89
379
  frequency_state *st = self->state;
90
380
  *out = NULL;
91
381
 
@@ -93,22 +383,46 @@ static int frequency_process(tf_step *self, tf_batch *in, tf_batch **out,
93
383
  int *col_indices;
94
384
  if (st->n_cols > 0) {
95
385
  n_keys = st->n_cols;
96
- col_indices = malloc(n_keys * sizeof(int));
97
- for (size_t k = 0; k < n_keys; k++)
386
+ col_indices = tf_mallocarray_checked(n_keys ? n_keys : 1, sizeof(int));
387
+ if (!col_indices) return TF_ERROR;
388
+ for (size_t k = 0; k < n_keys; k++) {
98
389
  col_indices[k] = tf_batch_col_index(in, st->cols[k]);
390
+ if (col_indices[k] < 0) {
391
+ char msg[256];
392
+ snprintf(msg, sizeof(msg), "frequency: column '%s' not found",
393
+ st->cols[k] ? st->cols[k] : "");
394
+ int err_rc = frequency_write_error(side, msg);
395
+ free(col_indices);
396
+ if (err_rc != TF_OK) return TF_ERROR;
397
+ return TF_ERROR;
398
+ }
399
+ }
99
400
  } else {
100
401
  n_keys = in->n_cols;
101
- col_indices = malloc(n_keys * sizeof(int));
402
+ col_indices = tf_mallocarray_checked(n_keys ? n_keys : 1, sizeof(int));
403
+ if (!col_indices) return TF_ERROR;
102
404
  for (size_t k = 0; k < n_keys; k++) col_indices[k] = (int)k;
103
405
  }
104
406
 
407
+ size_t row_base = st->row_index;
105
408
  for (size_t r = 0; r < in->n_rows; r++) {
106
409
  char *key = build_freq_key(in, r, col_indices, n_keys);
107
410
  if (!key) { free(col_indices); return TF_ERROR; }
108
- freq_add(&st->map, key);
411
+ size_t row_no = 0;
412
+ if (tf_size_add(row_base, r, &row_no) != TF_OK ||
413
+ tf_size_add(row_no, 1, &row_no) != TF_OK ||
414
+ freq_add(st, key, in, r, row_no, side) != 0) {
415
+ free(key);
416
+ free(col_indices);
417
+ return TF_ERROR;
418
+ }
109
419
  free(key);
110
420
  }
111
421
 
422
+ if (tf_size_add(st->row_index, in->n_rows, &st->row_index) != TF_OK) {
423
+ free(col_indices);
424
+ return TF_ERROR;
425
+ }
112
426
  free(col_indices);
113
427
  return TF_OK;
114
428
  }
@@ -118,75 +432,289 @@ static int frequency_flush(tf_step *self, tf_batch **out, tf_side_channels *side
118
432
  frequency_state *st = self->state;
119
433
  *out = NULL;
120
434
 
121
- if (st->map.count == 0) return TF_OK;
435
+ size_t n = 0;
436
+ if (tf_size_add(st->map.count, st->other_count > 0 ? 1u : 0u, &n) != TF_OK) return TF_ERROR;
437
+ if (n == 0) return TF_OK;
122
438
 
123
- /* Sort by count descending */
124
- size_t n = st->map.count;
125
- size_t *indices = malloc(n * sizeof(size_t));
126
- if (!indices) return TF_ERROR;
439
+ const char **keys = tf_mallocarray_checked(n, sizeof(char *));
440
+ size_t *counts = tf_mallocarray_checked(n, sizeof(size_t));
441
+ size_t *errors = tf_mallocarray_checked(n, sizeof(size_t));
442
+ size_t *indices = tf_mallocarray_checked(n, sizeof(size_t));
443
+ if (!keys || !counts || !errors || !indices) {
444
+ free(keys); free(counts); free(errors); free(indices);
445
+ return TF_ERROR;
446
+ }
447
+ for (size_t i = 0; i < st->map.count; i++) {
448
+ keys[i] = st->map.keys[i];
449
+ counts[i] = st->map.counts[i];
450
+ errors[i] = st->map.errors[i];
451
+ }
452
+ if (st->other_count > 0) {
453
+ keys[st->map.count] = st->other_label ? st->other_label : "__other__";
454
+ counts[st->map.count] = st->other_count;
455
+ errors[st->map.count] = 0;
456
+ }
127
457
  for (size_t i = 0; i < n; i++) indices[i] = i;
128
458
 
129
- /* Simple sort by count */
459
+ /* Simple sort by count desc, error asc, then value asc for deterministic ties. */
130
460
  for (size_t i = 0; i < n - 1; i++) {
131
461
  for (size_t j = i + 1; j < n; j++) {
132
- if (st->map.counts[indices[j]] > st->map.counts[indices[i]]) {
462
+ size_t a = indices[i], b = indices[j];
463
+ if (counts[b] > counts[a] ||
464
+ (counts[b] == counts[a] &&
465
+ (errors[b] < errors[a] ||
466
+ (errors[b] == errors[a] && strcmp(keys[b], keys[a]) < 0)))) {
133
467
  size_t tmp = indices[i]; indices[i] = indices[j]; indices[j] = tmp;
134
468
  }
135
469
  }
136
470
  }
137
471
 
138
- tf_batch *ob = tf_batch_create(2, n);
139
- if (!ob) { free(indices); return TF_ERROR; }
140
- tf_batch_set_schema(ob, 0, "value", TF_TYPE_STRING);
141
- tf_batch_set_schema(ob, 1, "count", TF_TYPE_INT64);
472
+ size_t out_cols = st->approximate ? 3u : 2u;
473
+ tf_batch *ob = tf_batch_create(out_cols, n);
474
+ if (!ob) { free(keys); free(counts); free(errors); free(indices); return TF_ERROR; }
475
+ if (tf_batch_set_schema(ob, 0, "value", TF_TYPE_STRING) != TF_OK ||
476
+ tf_batch_set_schema(ob, 1, "count", TF_TYPE_INT64) != TF_OK) {
477
+ tf_batch_free(ob);
478
+ free(keys); free(counts); free(errors); free(indices);
479
+ return TF_ERROR;
480
+ }
481
+ if (st->approximate &&
482
+ tf_batch_set_schema(ob, 2, "error", TF_TYPE_INT64) != TF_OK) {
483
+ tf_batch_free(ob);
484
+ free(keys); free(counts); free(errors); free(indices);
485
+ return TF_ERROR;
486
+ }
142
487
 
143
488
  for (size_t i = 0; i < n; i++) {
144
- tf_batch_ensure_capacity(ob, i + 1);
145
- tf_batch_set_string(ob, i, 0, st->map.keys[indices[i]]);
146
- tf_batch_set_int64(ob, i, 1, (int64_t)st->map.counts[indices[i]]);
147
- ob->n_rows = i + 1;
489
+ if (tf_batch_ensure_capacity(ob, i + 1) != TF_OK ||
490
+ tf_batch_set_string(ob, i, 0, keys[indices[i]]) != TF_OK ||
491
+ tf_batch_set_int64(ob, i, 1, (int64_t)counts[indices[i]]) != TF_OK) {
492
+ tf_batch_free(ob);
493
+ free(keys); free(counts); free(errors); free(indices);
494
+ return TF_ERROR;
495
+ }
496
+ if (st->approximate &&
497
+ tf_batch_set_int64(ob, i, 2, (int64_t)errors[indices[i]]) != TF_OK) {
498
+ tf_batch_free(ob);
499
+ free(keys); free(counts); free(errors); free(indices);
500
+ return TF_ERROR;
501
+ }
502
+ if (tf_batch_expose_row(ob, i) != TF_OK) {
503
+ tf_batch_free(ob);
504
+ free(keys); free(counts); free(errors); free(indices);
505
+ return TF_ERROR;
506
+ }
148
507
  }
149
508
 
150
- free(indices);
509
+ free(keys); free(counts); free(errors); free(indices);
151
510
  *out = ob;
152
511
  return TF_OK;
153
512
  }
154
513
 
155
- static void frequency_destroy(tf_step *self) {
514
+ static size_t frequency_retained_state_bytes(const frequency_state *st) {
515
+ if (!st) return 0;
516
+ size_t slot_width = 0;
517
+ size_t slot_bytes = 0;
518
+ size_t bytes = st->map.key_bytes;
519
+ if (tf_size_add(sizeof(char *), sizeof(size_t), &slot_width) != TF_OK ||
520
+ tf_size_add(slot_width, sizeof(size_t), &slot_width) != TF_OK ||
521
+ tf_size_mul(st->map.cap, slot_width, &slot_bytes) != TF_OK ||
522
+ tf_size_add(bytes, slot_bytes, &bytes) != TF_OK) {
523
+ return SIZE_MAX;
524
+ }
525
+ if (st->other_label) {
526
+ size_t label_bytes = 0;
527
+ if (tf_size_add(strlen(st->other_label), 1, &label_bytes) != TF_OK ||
528
+ tf_size_add(bytes, label_bytes, &bytes) != TF_OK) {
529
+ return SIZE_MAX;
530
+ }
531
+ }
532
+ return bytes;
533
+ }
534
+
535
+ static int frequency_append_stats(tf_step *self, tf_buffer *out) {
536
+ if (!self || !self->state || !out) return TF_ERROR;
156
537
  frequency_state *st = self->state;
538
+ char buf[420];
539
+ snprintf(buf, sizeof(buf),
540
+ ",\"tracked_values\":%zu,\"tracked_key_bytes\":%zu,"
541
+ "\"overflow_count\":%zu,\"retained_state_bytes\":%zu,\"max_state_bytes\":%zu",
542
+ st->map.count, st->map.key_bytes, st->other_count,
543
+ frequency_retained_state_bytes(st), st->max_state_bytes);
544
+ if (tf_buffer_write_str(out, buf) != TF_OK) return TF_ERROR;
545
+ if (st->approximate) {
546
+ snprintf(buf, sizeof(buf),
547
+ ",\"approximate\":true,\"approx_replacements\":%zu",
548
+ st->approx_replacements);
549
+ if (tf_buffer_write_str(out, buf) != TF_OK) return TF_ERROR;
550
+ }
551
+ return TF_OK;
552
+ }
553
+
554
+ static void frequency_state_free(frequency_state *st) {
157
555
  if (st) {
158
- for (size_t i = 0; i < st->n_cols; i++) free(st->cols[i]);
556
+ tf_audit_options_free(&st->audit_opts);
557
+ if (st->cols) {
558
+ for (size_t i = 0; i < st->n_cols; i++) free(st->cols[i]);
559
+ }
159
560
  free(st->cols);
561
+ free(st->other_label);
160
562
  for (size_t i = 0; i < st->map.count; i++) free(st->map.keys[i]);
161
- free(st->map.keys); free(st->map.counts);
563
+ free(st->map.keys); free(st->map.counts); free(st->map.errors);
162
564
  free(st);
163
565
  }
566
+ }
567
+
568
+ static void frequency_destroy(tf_step *self) {
569
+ if (self) frequency_state_free(self->state);
164
570
  free(self);
165
571
  }
166
572
 
167
573
  tf_step *tf_frequency_create(const cJSON *args) {
168
574
  frequency_state *st = calloc(1, sizeof(frequency_state));
169
575
  if (!st) return NULL;
576
+ tf_audit_options_init(&st->audit_opts, 1);
577
+ st->audit_limit = 1000;
170
578
 
171
579
  if (args) {
580
+ size_t parsed_size = 0;
581
+ int has_max_values = tf_json_get_size_arg(args, "max_values",
582
+ 1, TF_MAX_COUNT_ARG,
583
+ &parsed_size, "frequency");
584
+ if (has_max_values < 0) { frequency_state_free(st); return NULL; }
585
+ if (has_max_values > 0) st->max_values = parsed_size;
586
+
587
+ int has_max_state = tf_json_get_size_arg(args, "max_state_bytes",
588
+ 1, TF_MAX_STATE_BYTES,
589
+ &parsed_size, "frequency");
590
+ if (has_max_state < 0) { frequency_state_free(st); return NULL; }
591
+ if (has_max_state > 0) st->max_state_bytes = parsed_size;
592
+
593
+ int mode_approx = -1;
594
+ cJSON *mode = cJSON_GetObjectItemCaseSensitive(args, "mode");
595
+ if (mode) {
596
+ if (!cJSON_IsString(mode) || !mode->valuestring) {
597
+ tf_set_last_error("frequency: mode must be exact or approx");
598
+ frequency_state_free(st);
599
+ return NULL;
600
+ }
601
+ if (strcmp(mode->valuestring, "approx") == 0) {
602
+ mode_approx = 1;
603
+ } else if (strcmp(mode->valuestring, "exact") == 0) {
604
+ mode_approx = 0;
605
+ } else {
606
+ tf_set_last_error("frequency: mode must be exact or approx");
607
+ frequency_state_free(st);
608
+ return NULL;
609
+ }
610
+ }
611
+ cJSON *approx = cJSON_GetObjectItemCaseSensitive(args, "approx");
612
+ if (approx) {
613
+ if (!cJSON_IsBool(approx)) {
614
+ tf_set_last_error("frequency: approx must be true or false");
615
+ frequency_state_free(st);
616
+ return NULL;
617
+ }
618
+ int approx_value = cJSON_IsTrue(approx) ? 1 : 0;
619
+ if (mode_approx >= 0 && mode_approx != approx_value) {
620
+ tf_set_last_error("frequency: mode and approx conflict");
621
+ frequency_state_free(st);
622
+ return NULL;
623
+ }
624
+ st->approximate = approx_value;
625
+ } else if (mode_approx >= 0) {
626
+ st->approximate = mode_approx;
627
+ }
628
+
629
+ cJSON *audit_j = cJSON_GetObjectItemCaseSensitive(args, "audit");
630
+ st->audit = cJSON_IsTrue(audit_j) ? 1 : 0;
631
+ cJSON *audit_limit_j = cJSON_GetObjectItemCaseSensitive(args, "audit_limit");
632
+ if (!audit_limit_j) audit_limit_j = cJSON_GetObjectItemCaseSensitive(args, "auditLimit");
633
+ if (audit_limit_j) {
634
+ if (tf_json_get_size_arg_any(args, "audit_limit", "auditLimit",
635
+ 1, TF_MAX_AUDIT_RECORDS,
636
+ &parsed_size, "frequency") < 0) {
637
+ frequency_state_free(st);
638
+ return NULL;
639
+ }
640
+ st->audit_limit = parsed_size;
641
+ }
642
+
643
+ if (tf_audit_options_parse(&st->audit_opts, args, "frequency") != TF_OK) {
644
+ frequency_state_free(st);
645
+ return NULL;
646
+ }
647
+
648
+ cJSON *overflow = cJSON_GetObjectItemCaseSensitive(args, "overflow");
649
+ if (cJSON_IsString(overflow)) {
650
+ if (strcmp(overflow->valuestring, "other") == 0) {
651
+ st->overflow_other = 1;
652
+ } else if (strcmp(overflow->valuestring, "error") != 0) {
653
+ tf_set_last_error("frequency: overflow must be error or other");
654
+ frequency_state_free(st);
655
+ return NULL;
656
+ }
657
+ }
658
+ cJSON *other = cJSON_GetObjectItemCaseSensitive(args, "other");
659
+ const char *other_label = cJSON_IsString(other) ? other->valuestring : "__other__";
660
+ if (st->approximate) {
661
+ if (st->max_values == 0) {
662
+ tf_set_last_error("frequency: mode=approx requires max_values");
663
+ frequency_state_free(st);
664
+ return NULL;
665
+ }
666
+ if (st->overflow_other) {
667
+ tf_set_last_error("frequency: mode=approx and overflow=other are mutually exclusive");
668
+ frequency_state_free(st);
669
+ return NULL;
670
+ }
671
+ }
672
+ if (st->overflow_other) {
673
+ if (st->max_values == 0) {
674
+ tf_set_last_error("frequency: overflow=other requires max_values");
675
+ frequency_state_free(st);
676
+ return NULL;
677
+ }
678
+ if (!other_label || other_label[0] == '\0') {
679
+ tf_set_last_error("frequency: other label cannot be empty");
680
+ frequency_state_free(st);
681
+ return NULL;
682
+ }
683
+ st->other_label = strdup(other_label);
684
+ if (!st->other_label) { frequency_state_free(st); return NULL; }
685
+ }
686
+
172
687
  cJSON *columns = cJSON_GetObjectItemCaseSensitive(args, "columns");
173
- if (columns && cJSON_IsArray(columns)) {
688
+ if (columns) {
689
+ if (!cJSON_IsArray(columns)) {
690
+ tf_set_last_error("frequency: columns must be an array");
691
+ frequency_state_free(st);
692
+ return NULL;
693
+ }
174
694
  int n = cJSON_GetArraySize(columns);
175
695
  if (n > 0) {
176
- st->cols = calloc(n, sizeof(char *));
177
- st->n_cols = n;
696
+ st->cols = tf_callocarray_checked((size_t)n, sizeof(char *));
697
+ if (!st->cols) { frequency_state_free(st); return NULL; }
698
+ st->n_cols = (size_t)n;
178
699
  for (int i = 0; i < n; i++) {
179
700
  cJSON *item = cJSON_GetArrayItem(columns, i);
180
- if (cJSON_IsString(item)) st->cols[i] = strdup(item->valuestring);
701
+ if (!cJSON_IsString(item) || !item->valuestring || !item->valuestring[0]) {
702
+ tf_set_last_error("frequency: column names must be non-empty strings");
703
+ frequency_state_free(st);
704
+ return NULL;
705
+ }
706
+ st->cols[i] = strdup(item->valuestring);
707
+ if (!st->cols[i]) { frequency_state_free(st); return NULL; }
181
708
  }
182
709
  }
183
710
  }
184
711
  }
185
712
 
186
- tf_step *step = malloc(sizeof(tf_step));
187
- if (!step) { frequency_destroy(&(tf_step){.state = st}); return NULL; }
713
+ tf_step *step = calloc(1, sizeof(tf_step));
714
+ if (!step) { frequency_state_free(st); return NULL; }
188
715
  step->process = frequency_process;
189
716
  step->flush = frequency_flush;
717
+ step->append_stats = frequency_append_stats;
190
718
  step->destroy = frequency_destroy;
191
719
  step->state = st;
192
720
  return step;