tranfi 0.1.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 (132) hide show
  1. package/LICENSE +177 -0
  2. package/NOTICE +8 -0
  3. package/README.md +272 -40
  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 +348 -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 +8 -11
  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_onehot.c CHANGED
@@ -2,7 +2,11 @@
2
2
  * op_onehot.c — One-hot encoding of a categorical column.
3
3
  * Expands a single column into N binary (0/1) columns.
4
4
  *
5
- * Config: {"column": "city", "drop": false}
5
+ * Config: {"column": "city", "drop": false,
6
+ * "categories": ["Paris", "London"],
7
+ * "max_categories": 1000,
8
+ * "max_state_bytes": 1048576,
9
+ * "unknown": "error" | "other" | "null"}
6
10
  */
7
11
 
8
12
  #include "internal.h"
@@ -11,19 +15,68 @@
11
15
  #include <string.h>
12
16
  #include <stdio.h>
13
17
 
18
+ typedef enum {
19
+ TF_CAT_UNKNOWN_ADD,
20
+ TF_CAT_UNKNOWN_ERROR,
21
+ TF_CAT_UNKNOWN_OTHER,
22
+ TF_CAT_UNKNOWN_NULL
23
+ } tf_cat_unknown_policy;
24
+
14
25
  typedef struct {
15
26
  char *value; /* category value string */
16
27
  char *col_name; /* generated column name: "column_value" */
17
28
  } onehot_category;
18
29
 
19
30
  typedef struct {
20
- char *column;
21
- int drop; /* drop original column */
22
- onehot_category *cats;
23
- size_t n_cats;
24
- size_t cap;
31
+ char *column;
32
+ int drop; /* drop original column */
33
+ size_t max_categories; /* 0 = unlimited */
34
+ size_t max_state_bytes; /* 0 = unlimited */
35
+ int categories_declared;
36
+ tf_cat_unknown_policy unknown;
37
+ onehot_category *cats;
38
+ size_t n_cats;
39
+ size_t cap;
25
40
  } onehot_state;
26
41
 
42
+ static const char *OTHER_CATEGORY = "__other__";
43
+
44
+ static size_t onehot_retained_state_bytes(const onehot_state *st);
45
+
46
+ static int onehot_write_error(tf_side_channels *side, const char *msg) {
47
+ return tf_side_write_error(side, msg);
48
+ }
49
+
50
+ static int onehot_unknown_error(const onehot_state *st, const char *val,
51
+ tf_side_channels *side) {
52
+ char msg[256];
53
+ snprintf(msg, sizeof(msg),
54
+ "onehot: unknown category '%s' for column '%s'",
55
+ val ? val : "", st->column ? st->column : "");
56
+ return onehot_write_error(side, msg);
57
+ }
58
+
59
+ static int onehot_limit_error(const onehot_state *st, const char *val,
60
+ tf_side_channels *side) {
61
+ char msg[256];
62
+ snprintf(msg, sizeof(msg),
63
+ "onehot: max_categories=%zu exceeded while tracking category '%s'",
64
+ st->max_categories, val ? val : "");
65
+ return onehot_write_error(side, msg);
66
+ }
67
+
68
+ static int onehot_check_state_bytes(onehot_state *st, tf_side_channels *side) {
69
+ if (!st || st->max_state_bytes == 0) return TF_OK;
70
+ size_t retained = onehot_retained_state_bytes(st);
71
+ if (retained <= st->max_state_bytes) return TF_OK;
72
+ char msg[192];
73
+ snprintf(msg, sizeof(msg),
74
+ "onehot: max_state_bytes=%zu exceeded while tracking categories (%zu bytes retained)",
75
+ st->max_state_bytes, retained);
76
+ if (onehot_write_error(side, msg) != TF_OK) return TF_ERROR;
77
+ return TF_ERROR;
78
+ }
79
+
27
80
  static const char *get_string_value(const tf_batch *b, size_t r, int ci, char *buf, size_t bufsz) {
28
81
  if (tf_batch_is_null(b, r, ci)) return NULL;
29
82
  switch (b->col_types[ci]) {
@@ -40,127 +93,272 @@ static const char *get_string_value(const tf_batch *b, size_t r, int ci, char *b
40
93
  }
41
94
  }
42
95
 
43
- static int find_or_add_category(onehot_state *st, const char *val) {
96
+ static int find_category(const onehot_state *st, const char *val) {
44
97
  for (size_t i = 0; i < st->n_cats; i++) {
45
- if (strcmp(st->cats[i].value, val) == 0)
46
- return (int)i;
98
+ if (strcmp(st->cats[i].value, val) == 0) return (int)i;
47
99
  }
48
- /* Add new category */
100
+ return -1;
101
+ }
102
+
103
+ static int add_category(onehot_state *st, const char *val) {
104
+ int existing = find_category(st, val);
105
+ if (existing >= 0) return existing;
49
106
  if (st->n_cats >= st->cap) {
50
- size_t newcap = st->cap ? st->cap * 2 : 16;
51
- onehot_category *tmp = realloc(st->cats, newcap * sizeof(onehot_category));
107
+ size_t min_cap = 0, newcap = 0;
108
+ if (tf_size_add(st->n_cats, 1, &min_cap) != TF_OK ||
109
+ tf_size_grow_pow2(st->cap, min_cap, 16, &newcap) != TF_OK) {
110
+ return -1;
111
+ }
112
+ onehot_category *tmp = tf_reallocarray_checked(st->cats, newcap,
113
+ sizeof(onehot_category));
52
114
  if (!tmp) return -1;
53
115
  st->cats = tmp;
54
116
  st->cap = newcap;
55
117
  }
56
- st->cats[st->n_cats].value = strdup(val);
57
- char namebuf[512];
58
- snprintf(namebuf, sizeof(namebuf), "%s_%s", st->column, val);
59
- st->cats[st->n_cats].col_name = strdup(namebuf);
118
+ char *value = strdup(val);
119
+ if (!value) return -1;
120
+ size_t column_len = 0, value_len = 0, name_len = 0, name_cap = 0;
121
+ if (tf_string_length_bounded(st->column, TF_MAX_COLUMN_NAME_BYTES,
122
+ &column_len, "onehot", "column") != TF_OK ||
123
+ tf_string_length_bounded(val, TF_MAX_COLUMN_NAME_BYTES,
124
+ &value_len, "onehot", "category value") != TF_OK ||
125
+ tf_size_add(column_len, 1, &name_len) != TF_OK ||
126
+ tf_size_add(name_len, value_len, &name_len) != TF_OK ||
127
+ tf_check_byte_limit(name_len, TF_MAX_COLUMN_NAME_BYTES,
128
+ "onehot", "generated column name") != TF_OK ||
129
+ tf_size_add(name_len, 1, &name_cap) != TF_OK) {
130
+ free(value);
131
+ return -1;
132
+ }
133
+ char *col_name = tf_mallocarray_checked(name_cap, sizeof(char));
134
+ if (!col_name) {
135
+ free(value);
136
+ return -1;
137
+ }
138
+ memcpy(col_name, st->column, column_len);
139
+ col_name[column_len] = '_';
140
+ memcpy(col_name + column_len + 1, val, value_len);
141
+ col_name[name_len] = '\0';
142
+ st->cats[st->n_cats].value = value;
143
+ st->cats[st->n_cats].col_name = col_name;
60
144
  st->n_cats++;
61
145
  return (int)(st->n_cats - 1);
62
146
  }
63
147
 
148
+ static int resolve_other(onehot_state *st, int *idx, tf_side_channels *side) {
149
+ int other = find_category(st, OTHER_CATEGORY);
150
+ if (other >= 0) { *idx = other; return 0; }
151
+ if (st->max_categories > 0 && st->n_cats >= st->max_categories) {
152
+ if (onehot_limit_error(st, OTHER_CATEGORY, side) != TF_OK) return -1;
153
+ return -1;
154
+ }
155
+ other = add_category(st, OTHER_CATEGORY);
156
+ if (other < 0) return -1;
157
+ if (onehot_check_state_bytes(st, side) != TF_OK) return -1;
158
+ *idx = other;
159
+ return 0;
160
+ }
161
+
162
+ static int resolve_unknown(onehot_state *st, const char *val, int *idx,
163
+ tf_side_channels *side) {
164
+ switch (st->unknown) {
165
+ case TF_CAT_UNKNOWN_NULL:
166
+ *idx = -1;
167
+ return 0;
168
+ case TF_CAT_UNKNOWN_OTHER:
169
+ return resolve_other(st, idx, side);
170
+ case TF_CAT_UNKNOWN_ERROR:
171
+ if (onehot_unknown_error(st, val, side) != TF_OK) return -1;
172
+ return -1;
173
+ case TF_CAT_UNKNOWN_ADD:
174
+ default:
175
+ break;
176
+ }
177
+ return 1;
178
+ }
179
+
180
+ static int resolve_category(onehot_state *st, const char *val, int *idx,
181
+ tf_side_channels *side) {
182
+ *idx = -1;
183
+ if (!val) return 0;
184
+
185
+ int existing = find_category(st, val);
186
+ if (existing >= 0) { *idx = existing; return 0; }
187
+
188
+ if (st->categories_declared) {
189
+ int rc = resolve_unknown(st, val, idx, side);
190
+ return rc == 0 ? 0 : -1;
191
+ }
192
+
193
+ if (st->max_categories > 0 && st->unknown == TF_CAT_UNKNOWN_OTHER) {
194
+ int other = find_category(st, OTHER_CATEGORY);
195
+ size_t reserve = other >= 0 ? 0 : 1;
196
+ if (st->n_cats + reserve >= st->max_categories) {
197
+ return resolve_other(st, idx, side);
198
+ }
199
+ }
200
+
201
+ if (st->max_categories > 0 && st->n_cats >= st->max_categories) {
202
+ int rc = resolve_unknown(st, val, idx, side);
203
+ if (rc == 0) return 0;
204
+ if (onehot_limit_error(st, val, side) != TF_OK) return -1;
205
+ return -1;
206
+ }
207
+
208
+ int added = add_category(st, val);
209
+ if (added < 0) return -1;
210
+ if (onehot_check_state_bytes(st, side) != TF_OK) return -1;
211
+ *idx = added;
212
+ return 0;
213
+ }
214
+
64
215
  static int onehot_process(tf_step *self, tf_batch *in, tf_batch **out,
65
216
  tf_side_channels *side) {
66
- (void)side;
67
217
  onehot_state *st = self->state;
68
218
  *out = NULL;
69
219
 
70
220
  int ci = tf_batch_col_index(in, st->column);
221
+ size_t match_count = in->n_rows ? in->n_rows : 1;
222
+ int *matches = tf_mallocarray_checked(match_count, sizeof(int));
223
+ if (!matches) return TF_ERROR;
224
+ for (size_t r = 0; r < in->n_rows; r++) matches[r] = -1;
71
225
 
72
- /* First pass: discover any new categories in this batch */
73
- size_t cats_before = st->n_cats;
74
226
  if (ci >= 0) {
75
227
  char buf[64];
76
228
  for (size_t r = 0; r < in->n_rows; r++) {
77
229
  const char *val = get_string_value(in, r, ci, buf, sizeof(buf));
78
- if (val) find_or_add_category(st, val);
230
+ if (resolve_category(st, val, &matches[r], side) != 0) {
231
+ free(matches);
232
+ return TF_ERROR;
233
+ }
79
234
  }
80
235
  }
81
- (void)cats_before;
82
-
83
- /* Compute output column count */
84
- size_t out_cols = st->drop ? (in->n_cols - 1 + st->n_cats) : (in->n_cols + st->n_cats);
85
236
 
237
+ size_t input_cols = (st->drop && ci >= 0 && in->n_cols > 0) ? in->n_cols - 1 : in->n_cols;
238
+ size_t out_cols = 0;
239
+ if (tf_size_add(input_cols, st->n_cats, &out_cols) != TF_OK) {
240
+ free(matches);
241
+ return TF_ERROR;
242
+ }
86
243
  tf_batch *ob = tf_batch_create(out_cols, in->n_rows);
87
- if (!ob) return TF_ERROR;
244
+ if (!ob) { free(matches); return TF_ERROR; }
88
245
 
89
- /* Set schema: copy input cols (optionally skipping target), append onehot cols */
90
246
  size_t oc = 0;
91
247
  for (size_t c = 0; c < in->n_cols; c++) {
92
248
  if (st->drop && ci >= 0 && c == (size_t)ci) continue;
93
- tf_batch_set_schema(ob, oc, in->col_names[c], in->col_types[c]);
249
+ if (tf_batch_set_schema(ob, oc, in->col_names[c], in->col_types[c]) != TF_OK) goto fail;
94
250
  oc++;
95
251
  }
96
252
  for (size_t i = 0; i < st->n_cats; i++) {
97
- tf_batch_set_schema(ob, oc + i, st->cats[i].col_name, TF_TYPE_INT64);
253
+ if (tf_batch_set_schema(ob, oc + i, st->cats[i].col_name, TF_TYPE_INT64) != TF_OK) goto fail;
98
254
  }
99
255
 
100
- /* Fill rows */
101
- char buf[64];
102
256
  for (size_t r = 0; r < in->n_rows; r++) {
103
- /* Copy input columns */
104
257
  oc = 0;
105
258
  for (size_t c = 0; c < in->n_cols; c++) {
106
259
  if (st->drop && ci >= 0 && c == (size_t)ci) continue;
107
- if (tf_batch_is_null(in, r, c)) {
108
- tf_batch_set_null(ob, r, oc);
109
- } else {
110
- switch (in->col_types[c]) {
111
- case TF_TYPE_STRING:
112
- tf_batch_set_string(ob, r, oc, tf_batch_get_string(in, r, c)); break;
113
- case TF_TYPE_INT64:
114
- tf_batch_set_int64(ob, r, oc, tf_batch_get_int64(in, r, c)); break;
115
- case TF_TYPE_FLOAT64:
116
- tf_batch_set_float64(ob, r, oc, tf_batch_get_float64(in, r, c)); break;
117
- case TF_TYPE_BOOL:
118
- tf_batch_set_bool(ob, r, oc, tf_batch_get_bool(in, r, c)); break;
119
- case TF_TYPE_DATE:
120
- tf_batch_set_date(ob, r, oc, tf_batch_get_date(in, r, c)); break;
121
- case TF_TYPE_TIMESTAMP:
122
- tf_batch_set_timestamp(ob, r, oc, tf_batch_get_timestamp(in, r, c)); break;
123
- default: tf_batch_set_null(ob, r, oc); break;
124
- }
125
- }
260
+ if (tf_batch_copy_cell(ob, r, oc, in, r, c) != TF_OK) goto fail;
126
261
  oc++;
127
262
  }
128
263
 
129
- /* Set onehot columns */
130
- const char *val = (ci >= 0) ? get_string_value(in, r, ci, buf, sizeof(buf)) : NULL;
131
- int match = -1;
132
- if (val) {
133
- for (size_t i = 0; i < st->n_cats; i++) {
134
- if (strcmp(st->cats[i].value, val) == 0) { match = (int)i; break; }
135
- }
136
- }
264
+ int match = matches[r];
137
265
  for (size_t i = 0; i < st->n_cats; i++) {
138
- tf_batch_set_int64(ob, r, oc + i, (int)i == match ? 1 : 0);
266
+ if (tf_batch_set_int64(ob, r, oc + i, (int)i == match ? 1 : 0) != TF_OK) goto fail;
139
267
  }
140
-
141
- ob->n_rows = r + 1;
268
+ if (tf_batch_expose_row(ob, r) != TF_OK) goto fail;
142
269
  }
143
270
 
271
+ free(matches);
144
272
  *out = ob;
145
273
  return TF_OK;
274
+
275
+ fail:
276
+ free(matches);
277
+ tf_batch_free(ob);
278
+ return TF_ERROR;
146
279
  }
147
280
 
148
281
  static int onehot_flush(tf_step *self, tf_batch **out, tf_side_channels *side) {
149
282
  (void)self; (void)side; *out = NULL; return TF_OK;
150
283
  }
151
284
 
152
- static void onehot_destroy(tf_step *self) {
285
+ static size_t onehot_category_value_bytes(const onehot_state *st) {
286
+ size_t total = 0;
287
+ if (!st) return 0;
288
+ for (size_t i = 0; i < st->n_cats; i++) {
289
+ if (st->cats[i].value) total += strlen(st->cats[i].value) + 1;
290
+ }
291
+ return total;
292
+ }
293
+
294
+ static size_t onehot_category_output_name_bytes(const onehot_state *st) {
295
+ size_t total = 0;
296
+ if (!st) return 0;
297
+ for (size_t i = 0; i < st->n_cats; i++) {
298
+ if (st->cats[i].col_name) total += strlen(st->cats[i].col_name) + 1;
299
+ }
300
+ return total;
301
+ }
302
+
303
+ static size_t onehot_retained_state_bytes(const onehot_state *st) {
304
+ if (!st) return 0;
305
+ size_t total = st->cap * sizeof(onehot_category);
306
+ total += onehot_category_value_bytes(st);
307
+ total += onehot_category_output_name_bytes(st);
308
+ if (st->column) total += strlen(st->column) + 1;
309
+ return total;
310
+ }
311
+
312
+ static int onehot_append_stats(tf_step *self, tf_buffer *out) {
313
+ if (!self || !self->state || !out) return TF_ERROR;
153
314
  onehot_state *st = self->state;
154
- if (st) {
155
- for (size_t i = 0; i < st->n_cats; i++) {
156
- free(st->cats[i].value);
157
- free(st->cats[i].col_name);
158
- }
159
- free(st->cats);
160
- free(st->column);
161
- free(st);
315
+ char buf[320];
316
+ snprintf(buf, sizeof(buf),
317
+ ",\"tracked_categories\":%zu,\"category_value_bytes\":%zu,"
318
+ "\"category_output_name_bytes\":%zu,\"retained_state_bytes\":%zu,"
319
+ "\"max_state_bytes\":%zu",
320
+ st->n_cats, onehot_category_value_bytes(st),
321
+ onehot_category_output_name_bytes(st), onehot_retained_state_bytes(st),
322
+ st->max_state_bytes);
323
+ return tf_buffer_write_str(out, buf);
324
+ }
325
+
326
+ static void onehot_state_free(onehot_state *st) {
327
+ if (!st) return;
328
+ for (size_t i = 0; i < st->n_cats; i++) {
329
+ free(st->cats[i].value);
330
+ free(st->cats[i].col_name);
331
+ }
332
+ free(st->cats);
333
+ free(st->column);
334
+ free(st);
335
+ }
336
+
337
+ static void onehot_destroy(tf_step *self) {
338
+ if (self) {
339
+ onehot_state_free(self->state);
340
+ free(self);
162
341
  }
163
- free(self);
342
+ }
343
+
344
+ static int parse_unknown_policy(const cJSON *args, tf_cat_unknown_policy *policy,
345
+ int *specified) {
346
+ cJSON *unknown_j = cJSON_GetObjectItemCaseSensitive(args, "unknown");
347
+ *specified = 0;
348
+ if (!unknown_j) return 0;
349
+ if (!cJSON_IsString(unknown_j)) {
350
+ tf_set_last_error("onehot: unknown must be one of error, other, null");
351
+ return -1;
352
+ }
353
+ *specified = 1;
354
+ if (strcmp(unknown_j->valuestring, "error") == 0) *policy = TF_CAT_UNKNOWN_ERROR;
355
+ else if (strcmp(unknown_j->valuestring, "other") == 0) *policy = TF_CAT_UNKNOWN_OTHER;
356
+ else if (strcmp(unknown_j->valuestring, "null") == 0) *policy = TF_CAT_UNKNOWN_NULL;
357
+ else {
358
+ tf_set_last_error("onehot: unknown must be one of error, other, null");
359
+ return -1;
360
+ }
361
+ return 0;
164
362
  }
165
363
 
166
364
  tf_step *tf_onehot_create(const cJSON *args) {
@@ -171,14 +369,88 @@ tf_step *tf_onehot_create(const cJSON *args) {
171
369
  onehot_state *st = calloc(1, sizeof(onehot_state));
172
370
  if (!st) return NULL;
173
371
  st->column = strdup(col_j->valuestring);
372
+ if (!st->column) { onehot_state_free(st); return NULL; }
373
+ st->unknown = TF_CAT_UNKNOWN_ADD;
174
374
 
175
375
  cJSON *drop_j = cJSON_GetObjectItemCaseSensitive(args, "drop");
176
376
  st->drop = cJSON_IsBool(drop_j) && cJSON_IsTrue(drop_j) ? 1 : 0;
177
377
 
178
- tf_step *step = malloc(sizeof(tf_step));
179
- if (!step) { free(st->column); free(st); return NULL; }
378
+ size_t parsed_size = 0;
379
+ int has_max_categories = tf_json_get_size_arg(args, "max_categories",
380
+ 1, TF_MAX_COUNT_ARG,
381
+ &parsed_size, "onehot");
382
+ if (has_max_categories < 0) { onehot_state_free(st); return NULL; }
383
+ if (has_max_categories > 0) st->max_categories = parsed_size;
384
+
385
+ int has_max_state = tf_json_get_size_arg(args, "max_state_bytes",
386
+ 1, TF_MAX_STATE_BYTES,
387
+ &parsed_size, "onehot");
388
+ if (has_max_state < 0) { onehot_state_free(st); return NULL; }
389
+ if (has_max_state > 0) st->max_state_bytes = parsed_size;
390
+
391
+ int unknown_specified = 0;
392
+ if (parse_unknown_policy(args, &st->unknown, &unknown_specified) != 0) {
393
+ onehot_state_free(st);
394
+ return NULL;
395
+ }
396
+
397
+ cJSON *cats_j = cJSON_GetObjectItemCaseSensitive(args, "categories");
398
+ if (cats_j) {
399
+ if (!cJSON_IsArray(cats_j)) {
400
+ tf_set_last_error("onehot: categories must be an array");
401
+ onehot_state_free(st);
402
+ return NULL;
403
+ }
404
+ st->categories_declared = 1;
405
+ cJSON *item = NULL;
406
+ cJSON_ArrayForEach(item, cats_j) {
407
+ if (!cJSON_IsString(item)) {
408
+ tf_set_last_error("onehot: categories must contain strings");
409
+ onehot_state_free(st);
410
+ return NULL;
411
+ }
412
+ if (find_category(st, item->valuestring) < 0 &&
413
+ st->max_categories > 0 && st->n_cats >= st->max_categories) {
414
+ tf_set_last_error("onehot: categories exceed max_categories");
415
+ onehot_state_free(st);
416
+ return NULL;
417
+ }
418
+ if (add_category(st, item->valuestring) < 0) {
419
+ onehot_state_free(st);
420
+ return NULL;
421
+ }
422
+ if (onehot_check_state_bytes(st, NULL) != TF_OK) {
423
+ onehot_state_free(st);
424
+ return NULL;
425
+ }
426
+ }
427
+ }
428
+
429
+ if (st->categories_declared && !unknown_specified)
430
+ st->unknown = TF_CAT_UNKNOWN_ERROR;
431
+
432
+ if (st->categories_declared && st->unknown == TF_CAT_UNKNOWN_OTHER &&
433
+ find_category(st, OTHER_CATEGORY) < 0) {
434
+ if (st->max_categories > 0 && st->n_cats >= st->max_categories) {
435
+ tf_set_last_error("onehot: max_categories leaves no room for other category");
436
+ onehot_state_free(st);
437
+ return NULL;
438
+ }
439
+ if (add_category(st, OTHER_CATEGORY) < 0) {
440
+ onehot_state_free(st);
441
+ return NULL;
442
+ }
443
+ if (onehot_check_state_bytes(st, NULL) != TF_OK) {
444
+ onehot_state_free(st);
445
+ return NULL;
446
+ }
447
+ }
448
+
449
+ tf_step *step = calloc(1, sizeof(tf_step));
450
+ if (!step) { onehot_state_free(st); return NULL; }
180
451
  step->process = onehot_process;
181
452
  step->flush = onehot_flush;
453
+ step->append_stats = onehot_append_stats;
182
454
  step->destroy = onehot_destroy;
183
455
  step->state = st;
184
456
  return step;