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,721 @@
1
+ /*
2
+ * op_frequency.c — Value counts. Hash map → emit sorted by count desc.
3
+ *
4
+ * Config: {"columns": ["city"]}
5
+ * or {} for frequency of all columns concatenated.
6
+ */
7
+
8
+ #include "internal.h"
9
+ #include "cJSON.h"
10
+ #include "date_utils.h"
11
+ #include <stdlib.h>
12
+ #include <string.h>
13
+ #include <stdio.h>
14
+
15
+ typedef struct {
16
+ char **keys;
17
+ size_t *counts;
18
+ size_t *errors;
19
+ size_t count;
20
+ size_t cap;
21
+ size_t key_bytes;
22
+ } freq_map;
23
+
24
+ typedef struct {
25
+ char **cols;
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;
39
+ freq_map map;
40
+ } frequency_state;
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
+
86
+ static char *build_freq_key(const tf_batch *b, size_t row,
87
+ int *col_indices, size_t n_keys) {
88
+ size_t buf_cap = 256;
89
+ char *buf = tf_mallocarray_checked(buf_cap, sizeof(char));
90
+ if (!buf) return NULL;
91
+ size_t buf_len = 0;
92
+ for (size_t k = 0; k < n_keys; k++) {
93
+ int c = col_indices[k];
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
+ }
101
+ char val_buf[64];
102
+ const char *val;
103
+ size_t val_len;
104
+ if (c < 0 || tf_batch_is_null(b, row, c)) {
105
+ val = "\\N"; val_len = 2;
106
+ } else {
107
+ switch (b->col_types[c]) {
108
+ case TF_TYPE_STRING: val = tf_batch_get_string(b, row, c); val_len = strlen(val); break;
109
+ case TF_TYPE_INT64:
110
+ val_len = snprintf(val_buf, sizeof(val_buf), "%lld", (long long)tf_batch_get_int64(b, row, c));
111
+ val = val_buf; break;
112
+ case TF_TYPE_FLOAT64:
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);
118
+ val = val_buf; break;
119
+ case TF_TYPE_BOOL:
120
+ val = tf_batch_get_bool(b, row, c) ? "T" : "F"; val_len = 1; break;
121
+ case TF_TYPE_DATE:
122
+ val_len = snprintf(val_buf, sizeof(val_buf), "%d", (int)tf_batch_get_date(b, row, c));
123
+ val = val_buf; break;
124
+ case TF_TYPE_TIMESTAMP:
125
+ val_len = snprintf(val_buf, sizeof(val_buf), "%lld", (long long)tf_batch_get_timestamp(b, row, c));
126
+ val = val_buf; break;
127
+ default: val = "\\N"; val_len = 2; break;
128
+ }
129
+ }
130
+ if (freq_buf_ensure(&buf, &buf_cap, buf_len, val_len) != TF_OK) {
131
+ free(buf);
132
+ return NULL;
133
+ }
134
+ memcpy(buf + buf_len, val, val_len);
135
+ buf_len += val_len;
136
+ }
137
+ buf[buf_len] = '\0';
138
+ return buf;
139
+ }
140
+
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) {
223
+ for (size_t i = 0; i < map->count; i++) {
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;
245
+ }
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;
286
+ }
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;
291
+ map->count++;
292
+ return 0;
293
+ }
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
+
377
+ static int frequency_process(tf_step *self, tf_batch *in, tf_batch **out,
378
+ tf_side_channels *side) {
379
+ frequency_state *st = self->state;
380
+ *out = NULL;
381
+
382
+ size_t n_keys;
383
+ int *col_indices;
384
+ if (st->n_cols > 0) {
385
+ n_keys = st->n_cols;
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++) {
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
+ }
400
+ } else {
401
+ n_keys = in->n_cols;
402
+ col_indices = tf_mallocarray_checked(n_keys ? n_keys : 1, sizeof(int));
403
+ if (!col_indices) return TF_ERROR;
404
+ for (size_t k = 0; k < n_keys; k++) col_indices[k] = (int)k;
405
+ }
406
+
407
+ size_t row_base = st->row_index;
408
+ for (size_t r = 0; r < in->n_rows; r++) {
409
+ char *key = build_freq_key(in, r, col_indices, n_keys);
410
+ if (!key) { free(col_indices); return TF_ERROR; }
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
+ }
419
+ free(key);
420
+ }
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
+ }
426
+ free(col_indices);
427
+ return TF_OK;
428
+ }
429
+
430
+ static int frequency_flush(tf_step *self, tf_batch **out, tf_side_channels *side) {
431
+ (void)side;
432
+ frequency_state *st = self->state;
433
+ *out = NULL;
434
+
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;
438
+
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
+ }
457
+ for (size_t i = 0; i < n; i++) indices[i] = i;
458
+
459
+ /* Simple sort by count desc, error asc, then value asc for deterministic ties. */
460
+ for (size_t i = 0; i < n - 1; i++) {
461
+ for (size_t j = i + 1; j < n; j++) {
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)))) {
467
+ size_t tmp = indices[i]; indices[i] = indices[j]; indices[j] = tmp;
468
+ }
469
+ }
470
+ }
471
+
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
+ }
487
+
488
+ for (size_t i = 0; i < n; i++) {
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
+ }
507
+ }
508
+
509
+ free(keys); free(counts); free(errors); free(indices);
510
+ *out = ob;
511
+ return TF_OK;
512
+ }
513
+
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;
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) {
555
+ if (st) {
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
+ }
560
+ free(st->cols);
561
+ free(st->other_label);
562
+ for (size_t i = 0; i < st->map.count; i++) free(st->map.keys[i]);
563
+ free(st->map.keys); free(st->map.counts); free(st->map.errors);
564
+ free(st);
565
+ }
566
+ }
567
+
568
+ static void frequency_destroy(tf_step *self) {
569
+ if (self) frequency_state_free(self->state);
570
+ free(self);
571
+ }
572
+
573
+ tf_step *tf_frequency_create(const cJSON *args) {
574
+ frequency_state *st = calloc(1, sizeof(frequency_state));
575
+ if (!st) return NULL;
576
+ tf_audit_options_init(&st->audit_opts, 1);
577
+ st->audit_limit = 1000;
578
+
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
+
687
+ cJSON *columns = cJSON_GetObjectItemCaseSensitive(args, "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
+ }
694
+ int n = cJSON_GetArraySize(columns);
695
+ if (n > 0) {
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;
699
+ for (int i = 0; i < n; i++) {
700
+ cJSON *item = cJSON_GetArrayItem(columns, i);
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; }
708
+ }
709
+ }
710
+ }
711
+ }
712
+
713
+ tf_step *step = calloc(1, sizeof(tf_step));
714
+ if (!step) { frequency_state_free(st); return NULL; }
715
+ step->process = frequency_process;
716
+ step->flush = frequency_flush;
717
+ step->append_stats = frequency_append_stats;
718
+ step->destroy = frequency_destroy;
719
+ step->state = st;
720
+ return step;
721
+ }