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
@@ -0,0 +1,923 @@
1
+ #include "transform_internal.h"
2
+
3
+ #include <stdint.h>
4
+ #include <stdlib.h>
5
+ #include <string.h>
6
+
7
+ typedef struct tf_transform_category_slot {
8
+ uint64_t bits;
9
+ uint64_t count;
10
+ int occupied;
11
+ } tf_transform_category_slot;
12
+
13
+ struct tf_transform_category_store {
14
+ tf_transform_category_slot *slots;
15
+ size_t capacity;
16
+ size_t category_count;
17
+ uint64_t observed;
18
+ uint32_t dtype;
19
+ int inference_numeric;
20
+ };
21
+
22
+ static tf_transform_code category_poll(
23
+ const tf_transform_runtime_copy *runtime, tf_transform_error **error) {
24
+ tf_transform_code code = tf_transform_poll_cancel(runtime, error);
25
+ if (code != TF_TRANSFORM_OK) return code;
26
+ return tf_transform_check_runtime_fp(error);
27
+ }
28
+
29
+ static tf_transform_code fixed_category_known(
30
+ const tf_transform_recipe_column *recipe, uint64_t bits,
31
+ const tf_transform_runtime_copy *runtime, int *known,
32
+ tf_transform_error **error) {
33
+ /* Borrow immutable recipe storage; lookup never owns or modifies it. */
34
+ tf_transform_categorical_state dictionary = {0};
35
+ size_t ordinal = 0;
36
+ dictionary.categories = recipe->categorical_fixed;
37
+ dictionary.category_count = recipe->categorical_fixed_count;
38
+ dictionary.source_dtype = recipe->categorical_fixed_dtype;
39
+ return tf_transform_category_lookup(
40
+ &dictionary, bits, runtime, &ordinal, known, error);
41
+ }
42
+
43
+ static uint64_t category_hash(uint64_t value) {
44
+ value ^= value >> 30;
45
+ value *= UINT64_C(0xbf58476d1ce4e5b9);
46
+ value ^= value >> 27;
47
+ value *= UINT64_C(0x94d049bb133111eb);
48
+ value ^= value >> 31;
49
+ return value;
50
+ }
51
+
52
+ tf_transform_code tf_transform_category_key(
53
+ double value, uint32_t dtype, uint64_t *out, tf_transform_error **error) {
54
+ if (!out || (dtype != TF_VIEW_FLOAT32 && dtype != TF_VIEW_FLOAT64))
55
+ return tf_transform_set_error(
56
+ error, TF_TRANSFORM_INVALID_ARGUMENT,
57
+ "invalid categorical value type");
58
+ if (!tf_transform_double_is_finite(value))
59
+ return tf_transform_set_error(
60
+ error, TF_TRANSFORM_NUMERIC_DOMAIN,
61
+ "categorical value is not finite");
62
+ if (value == 0.0) {
63
+ *out = 0;
64
+ return TF_TRANSFORM_OK;
65
+ }
66
+ if (dtype == TF_VIEW_FLOAT32) {
67
+ float narrowed = (float)value;
68
+ uint32_t bits;
69
+ memcpy(&bits, &narrowed, sizeof(bits));
70
+ *out = bits;
71
+ } else {
72
+ *out = tf_transform_double_bits(value);
73
+ }
74
+ return TF_TRANSFORM_OK;
75
+ }
76
+
77
+ double tf_transform_category_decode(uint64_t bits, uint32_t dtype) {
78
+ if (dtype == TF_VIEW_FLOAT32) {
79
+ uint32_t narrowed_bits = (uint32_t)bits;
80
+ float narrowed;
81
+ memcpy(&narrowed, &narrowed_bits, sizeof(narrowed));
82
+ return (double)narrowed;
83
+ }
84
+ return tf_transform_double_from_bits(bits);
85
+ }
86
+
87
+ int tf_transform_category_compare(uint64_t left, uint64_t right, uint32_t dtype) {
88
+ uint64_t sign;
89
+ uint64_t mask;
90
+ uint64_t left_key;
91
+ uint64_t right_key;
92
+ if (dtype == TF_VIEW_FLOAT32) {
93
+ sign = UINT64_C(0x80000000);
94
+ mask = UINT64_C(0xffffffff);
95
+ } else {
96
+ sign = UINT64_C(0x8000000000000000);
97
+ mask = UINT64_MAX;
98
+ }
99
+ left &= mask;
100
+ right &= mask;
101
+ left_key = (left & sign) ? (~left & mask) : (left ^ sign);
102
+ right_key = (right & sign) ? (~right & mask) : (right ^ sign);
103
+ if (left_key < right_key) return -1;
104
+ if (left_key > right_key) return 1;
105
+ return 0;
106
+ }
107
+
108
+ tf_transform_code tf_transform_category_stores_requirements(
109
+ size_t column_count, uint64_t *resident_bytes,
110
+ uint64_t *allocation_count, tf_transform_error **error) {
111
+ if (!resident_bytes || !allocation_count || column_count == 0)
112
+ return tf_transform_set_error(
113
+ error, TF_TRANSFORM_INTERNAL,
114
+ "categorical store requirements are invalid");
115
+ if (column_count > SIZE_MAX / sizeof(tf_transform_category_store))
116
+ return tf_transform_set_error(
117
+ error, TF_TRANSFORM_RESOURCE_LIMIT,
118
+ "categorical store size overflows");
119
+ *resident_bytes = (uint64_t)(
120
+ column_count * sizeof(tf_transform_category_store));
121
+ *allocation_count = 1;
122
+ return TF_TRANSFORM_OK;
123
+ }
124
+
125
+ static void category_store_clear(tf_transform_category_store *store) {
126
+ if (!store) return;
127
+ free(store->slots);
128
+ memset(store, 0, sizeof(*store));
129
+ }
130
+
131
+ void tf_transform_category_stores_clear(tf_transform_analyzer *analyzer) {
132
+ if (!analyzer || !analyzer->category_stores) return;
133
+ for (size_t i = 0; i < analyzer->category_stores_initialized; ++i)
134
+ category_store_clear(&analyzer->category_stores[i]);
135
+ free(analyzer->category_stores);
136
+ analyzer->category_stores = NULL;
137
+ analyzer->category_stores_initialized = 0;
138
+ analyzer->total_categories = 0;
139
+ }
140
+
141
+ tf_transform_code tf_transform_category_stores_init(
142
+ tf_transform_analyzer *analyzer, tf_transform_error **error) {
143
+ size_t bytes;
144
+ void *memory = NULL;
145
+ tf_transform_code code;
146
+ if (!analyzer || analyzer->input_schema.field_count == 0)
147
+ return tf_transform_set_error(
148
+ error, TF_TRANSFORM_INTERNAL,
149
+ "categorical store initialization is invalid");
150
+ if (analyzer->input_schema.field_count
151
+ > SIZE_MAX / sizeof(*analyzer->category_stores))
152
+ return tf_transform_set_error(
153
+ error, TF_TRANSFORM_RESOURCE_LIMIT,
154
+ "categorical store size overflows");
155
+ bytes = analyzer->input_schema.field_count
156
+ * sizeof(*analyzer->category_stores);
157
+ code = tf_transform_analyzer_allocate_retained(
158
+ analyzer, bytes, &memory, error);
159
+ if (code != TF_TRANSFORM_OK) return code;
160
+ analyzer->category_stores = (tf_transform_category_store *)memory;
161
+ for (size_t i = 0; i < analyzer->input_schema.field_count; ++i) {
162
+ if (i % TF_TRANSFORM_CANCEL_ITERS_V1 == 0) {
163
+ code = category_poll(&analyzer->runtime, error);
164
+ if (code != TF_TRANSFORM_OK) return code;
165
+ }
166
+ memset(&analyzer->category_stores[i], 0,
167
+ sizeof(analyzer->category_stores[i]));
168
+ analyzer->category_stores_initialized = i + 1;
169
+ if (analyzer->recipe->columns[i].kind
170
+ != TF_TRANSFORM_KIND_NUMERIC)
171
+ analyzer->category_stores[i].dtype
172
+ = analyzer->input_schema.fields[i].dtype;
173
+ }
174
+ return category_poll(&analyzer->runtime, error);
175
+ }
176
+
177
+ static tf_transform_code category_slots_zero(
178
+ tf_transform_category_slot *slots, size_t capacity,
179
+ const tf_transform_runtime_copy *runtime,
180
+ tf_transform_error **error) {
181
+ for (size_t i = 0; i < capacity; ++i) {
182
+ if (i % TF_TRANSFORM_CANCEL_ITERS_V1 == 0) {
183
+ tf_transform_code code = category_poll(runtime, error);
184
+ if (code != TF_TRANSFORM_OK) return code;
185
+ }
186
+ memset(&slots[i], 0, sizeof(slots[i]));
187
+ }
188
+ return category_poll(runtime, error);
189
+ }
190
+
191
+ static tf_transform_code category_find_slot(
192
+ const tf_transform_category_store *store, uint64_t bits,
193
+ const tf_transform_runtime_copy *runtime,
194
+ size_t *slot_index, int *found, tf_transform_error **error) {
195
+ size_t mask;
196
+ size_t index;
197
+ if (slot_index) *slot_index = 0;
198
+ if (found) *found = 0;
199
+ if (!store || !store->slots || store->capacity == 0
200
+ || (store->capacity & (store->capacity - 1)) != 0
201
+ || !slot_index || !found)
202
+ return tf_transform_set_error(
203
+ error, TF_TRANSFORM_INTERNAL,
204
+ "categorical hash table is invalid");
205
+ mask = store->capacity - 1;
206
+ index = (size_t)(category_hash(bits) & (uint64_t)mask);
207
+ for (size_t probe = 0; probe < store->capacity; ++probe) {
208
+ const tf_transform_category_slot *slot;
209
+ if (probe % TF_TRANSFORM_CANCEL_ITERS_V1 == 0) {
210
+ tf_transform_code code = category_poll(runtime, error);
211
+ if (code != TF_TRANSFORM_OK) return code;
212
+ }
213
+ slot = &store->slots[index];
214
+ if (!slot->occupied) {
215
+ *slot_index = index;
216
+ *found = 0;
217
+ return TF_TRANSFORM_OK;
218
+ }
219
+ if (slot->bits == bits) {
220
+ *slot_index = index;
221
+ *found = 1;
222
+ return TF_TRANSFORM_OK;
223
+ }
224
+ index = (index + 1) & mask;
225
+ }
226
+ return tf_transform_set_error(
227
+ error, TF_TRANSFORM_INTERNAL,
228
+ "categorical hash table has no free slot");
229
+ }
230
+
231
+ static tf_transform_code category_store_resize(
232
+ tf_transform_analyzer *analyzer, tf_transform_category_store *store,
233
+ size_t capacity, tf_transform_error **error) {
234
+ tf_transform_category_slot *replacement = NULL;
235
+ tf_transform_category_slot *previous;
236
+ size_t previous_capacity;
237
+ size_t bytes;
238
+ size_t previous_bytes;
239
+ tf_transform_code code;
240
+ if (!store || ((store->slots == NULL) != (store->capacity == 0)))
241
+ return tf_transform_set_error(
242
+ error, TF_TRANSFORM_INTERNAL,
243
+ "categorical hash storage is inconsistent");
244
+ if (capacity == 0 || (capacity & (capacity - 1)) != 0
245
+ || capacity > SIZE_MAX / sizeof(*replacement))
246
+ return tf_transform_set_error(
247
+ error, TF_TRANSFORM_RESOURCE_LIMIT,
248
+ "categorical hash capacity overflows");
249
+ bytes = capacity * sizeof(*replacement);
250
+ code = tf_transform_analyzer_allocate_retained(
251
+ analyzer, bytes, (void **)&replacement, error);
252
+ if (code != TF_TRANSFORM_OK) return code;
253
+ code = category_slots_zero(
254
+ replacement, capacity, &analyzer->runtime, error);
255
+ if (code != TF_TRANSFORM_OK) goto failed;
256
+ previous = store->slots;
257
+ previous_capacity = store->capacity;
258
+ previous_bytes = previous_capacity * sizeof(*previous);
259
+ store->slots = replacement;
260
+ store->capacity = capacity;
261
+ for (size_t i = 0; i < previous_capacity; ++i) {
262
+ size_t target = 0;
263
+ int found = 0;
264
+ if (i % TF_TRANSFORM_CANCEL_ITERS_V1 == 0) {
265
+ code = category_poll(&analyzer->runtime, error);
266
+ if (code != TF_TRANSFORM_OK) {
267
+ store->slots = previous;
268
+ store->capacity = previous_capacity;
269
+ goto failed;
270
+ }
271
+ }
272
+ if (!previous[i].occupied) continue;
273
+ code = category_find_slot(
274
+ store, previous[i].bits, &analyzer->runtime,
275
+ &target, &found, error);
276
+ if (code != TF_TRANSFORM_OK || found) {
277
+ if (code == TF_TRANSFORM_OK)
278
+ code = tf_transform_set_error(
279
+ error, TF_TRANSFORM_INTERNAL,
280
+ "categorical rehash found a duplicate key");
281
+ store->slots = previous;
282
+ store->capacity = previous_capacity;
283
+ goto failed;
284
+ }
285
+ store->slots[target] = previous[i];
286
+ }
287
+ free(previous);
288
+ analyzer->resident_state_bytes -= (uint64_t)previous_bytes;
289
+ return category_poll(&analyzer->runtime, error);
290
+ failed:
291
+ free(replacement);
292
+ analyzer->resident_state_bytes -= (uint64_t)bytes;
293
+ return code;
294
+ }
295
+
296
+ tf_transform_code tf_transform_category_observe(
297
+ tf_transform_analyzer *analyzer, size_t column_index,
298
+ double value, uint32_t dtype, tf_transform_error **error) {
299
+ tf_transform_category_store *store;
300
+ uint64_t bits = 0;
301
+ size_t slot_index = 0;
302
+ int found = 0;
303
+ tf_transform_code code;
304
+ if (!analyzer || !analyzer->category_stores
305
+ || column_index >= analyzer->input_schema.field_count)
306
+ return tf_transform_set_error(
307
+ error, TF_TRANSFORM_INTERNAL,
308
+ "categorical analyzer state is unavailable");
309
+ store = &analyzer->category_stores[column_index];
310
+ if (store->dtype != dtype)
311
+ return tf_transform_set_error(
312
+ error, TF_TRANSFORM_INTERNAL,
313
+ "categorical analyzer dtype drifted");
314
+ if ((store->slots == NULL) != (store->capacity == 0)
315
+ || store->category_count > store->capacity / 2)
316
+ return tf_transform_set_error(
317
+ error, TF_TRANSFORM_INTERNAL,
318
+ "categorical analyzer hash state is inconsistent");
319
+ code = tf_transform_category_key(value, dtype, &bits, error);
320
+ if (code != TF_TRANSFORM_OK) return code;
321
+ const tf_transform_recipe_column *recipe = &analyzer->recipe->columns[column_index];
322
+ if (recipe->categorical_fixed_count) {
323
+ int known = 0;
324
+ code = fixed_category_known(recipe, bits, &analyzer->runtime, &known, error);
325
+ if (code != TF_TRANSFORM_OK) return code;
326
+ if (!known) {
327
+ if (recipe->categorical_unknown == TF_TRANSFORM_UNKNOWN_ERROR
328
+ || recipe->categorical_unknown == TF_TRANSFORM_UNKNOWN_NONE)
329
+ return tf_transform_set_error(error, TF_TRANSFORM_UNKNOWN_CATEGORY,
330
+ "analyzed value is outside the fixed dictionary");
331
+ if (store->observed == UINT64_MAX)
332
+ return tf_transform_set_error(error, TF_TRANSFORM_RESOURCE_LIMIT,
333
+ "categorical observed count overflows");
334
+ /* Accepted unknowns count as observations, but never as mode votes. */
335
+ ++store->observed;
336
+ return TF_TRANSFORM_OK;
337
+ }
338
+ if (recipe->categorical_impute == TF_TRANSFORM_CATEGORICAL_IMPUTE_NONE) {
339
+ /* The dictionary is already frozen; only mode needs learned counts. */
340
+ if (store->observed == UINT64_MAX)
341
+ return tf_transform_set_error(error, TF_TRANSFORM_RESOURCE_LIMIT,
342
+ "categorical observed count overflows");
343
+ ++store->observed;
344
+ return TF_TRANSFORM_OK;
345
+ }
346
+ }
347
+ if (store->slots) {
348
+ code = category_find_slot(
349
+ store, bits, &analyzer->runtime,
350
+ &slot_index, &found, error);
351
+ if (code != TF_TRANSFORM_OK) return code;
352
+ if (found) {
353
+ if (store->slots[slot_index].count == UINT64_MAX
354
+ || store->observed == UINT64_MAX)
355
+ return tf_transform_set_error(
356
+ error, TF_TRANSFORM_RESOURCE_LIMIT,
357
+ "categorical count overflows");
358
+ ++store->slots[slot_index].count;
359
+ ++store->observed;
360
+ return TF_TRANSFORM_OK;
361
+ }
362
+ }
363
+ if ((uint64_t)store->category_count
364
+ >= analyzer->runtime.limits.max_categories_per_column
365
+ || analyzer->total_categories
366
+ >= analyzer->runtime.limits.max_total_categories)
367
+ return tf_transform_set_error(
368
+ error, TF_TRANSFORM_RESOURCE_LIMIT,
369
+ "categorical discovery exceeds category limits");
370
+ if (!store->slots || (store->category_count + 1) > store->capacity / 2) {
371
+ size_t next_capacity = store->capacity == 0 ? 16 : store->capacity;
372
+ while ((store->category_count + 1) > next_capacity / 2) {
373
+ if (next_capacity > SIZE_MAX / 2)
374
+ return tf_transform_set_error(
375
+ error, TF_TRANSFORM_RESOURCE_LIMIT,
376
+ "categorical hash capacity overflows");
377
+ next_capacity *= 2;
378
+ }
379
+ code = category_store_resize(analyzer, store, next_capacity, error);
380
+ if (code != TF_TRANSFORM_OK) return code;
381
+ }
382
+ if (!store->slots || store->capacity == 0)
383
+ return tf_transform_set_error(
384
+ error, TF_TRANSFORM_INTERNAL,
385
+ "categorical analyzer hash storage is unavailable");
386
+ code = category_find_slot(
387
+ store, bits, &analyzer->runtime, &slot_index, &found, error);
388
+ if (code != TF_TRANSFORM_OK) return code;
389
+ if (found)
390
+ return tf_transform_set_error(
391
+ error, TF_TRANSFORM_INTERNAL,
392
+ "categorical key appeared during insertion");
393
+ if (store->observed == UINT64_MAX)
394
+ return tf_transform_set_error(
395
+ error, TF_TRANSFORM_RESOURCE_LIMIT,
396
+ "categorical observed count overflows");
397
+ store->slots[slot_index].bits = bits;
398
+ store->slots[slot_index].count = 1;
399
+ store->slots[slot_index].occupied = 1;
400
+ ++store->category_count;
401
+ ++store->observed;
402
+ ++analyzer->total_categories;
403
+ return TF_TRANSFORM_OK;
404
+ }
405
+
406
+ static tf_transform_code category_inference_resolve_numeric(
407
+ tf_transform_analyzer *analyzer, tf_transform_category_store *store,
408
+ tf_transform_error **error) {
409
+ uint64_t slot_bytes;
410
+ uint32_t dtype;
411
+ if (!analyzer || !store)
412
+ return tf_transform_set_error(
413
+ error, TF_TRANSFORM_INTERNAL,
414
+ "categorical inference state is unavailable");
415
+ if (store->inference_numeric) return TF_TRANSFORM_OK;
416
+ if (store->capacity > SIZE_MAX / sizeof(*store->slots))
417
+ return tf_transform_set_error(
418
+ error, TF_TRANSFORM_INTERNAL,
419
+ "categorical inference capacity is inconsistent");
420
+ slot_bytes = (uint64_t)(store->capacity * sizeof(*store->slots));
421
+ if (slot_bytes > analyzer->resident_state_bytes
422
+ || (uint64_t)store->category_count > analyzer->total_categories)
423
+ return tf_transform_set_error(
424
+ error, TF_TRANSFORM_INTERNAL,
425
+ "categorical inference accounting is inconsistent");
426
+ dtype = store->dtype;
427
+ free(store->slots);
428
+ analyzer->resident_state_bytes -= slot_bytes;
429
+ analyzer->total_categories -= (uint64_t)store->category_count;
430
+ memset(store, 0, sizeof(*store));
431
+ store->dtype = dtype;
432
+ store->inference_numeric = 1;
433
+ return category_poll(&analyzer->runtime, error);
434
+ }
435
+
436
+ tf_transform_code tf_transform_category_infer_observe(
437
+ tf_transform_analyzer *analyzer, size_t column_index,
438
+ double value, uint32_t dtype, int is_integer, uint64_t max_categories,
439
+ int *resolved_numeric, tf_transform_error **error) {
440
+ tf_transform_category_store *store;
441
+ uint64_t bits = 0;
442
+ size_t slot_index = 0;
443
+ int found = 0;
444
+ tf_transform_code code;
445
+ if (!analyzer || !analyzer->category_stores
446
+ || column_index >= analyzer->input_schema.field_count
447
+ || analyzer->recipe->columns[column_index].kind
448
+ != TF_TRANSFORM_KIND_INFER
449
+ || max_categories < 2 || !resolved_numeric)
450
+ return tf_transform_set_error(
451
+ error, TF_TRANSFORM_INTERNAL,
452
+ "categorical inference arguments are invalid");
453
+ store = &analyzer->category_stores[column_index];
454
+ *resolved_numeric = 0;
455
+ if (store->dtype != dtype)
456
+ return tf_transform_set_error(
457
+ error, TF_TRANSFORM_INTERNAL,
458
+ "categorical inference dtype drifted");
459
+ if (store->inference_numeric) {
460
+ *resolved_numeric = 1;
461
+ return TF_TRANSFORM_OK;
462
+ }
463
+ if (!is_integer) {
464
+ code = category_inference_resolve_numeric(analyzer, store, error);
465
+ if (code == TF_TRANSFORM_OK) *resolved_numeric = 1;
466
+ return code;
467
+ }
468
+ code = tf_transform_category_key(value, dtype, &bits, error);
469
+ if (code != TF_TRANSFORM_OK) return code;
470
+ if (store->slots) {
471
+ code = category_find_slot(
472
+ store, bits, &analyzer->runtime, &slot_index, &found, error);
473
+ if (code != TF_TRANSFORM_OK) return code;
474
+ }
475
+ if (found) {
476
+ if (store->slots[slot_index].count == UINT64_MAX
477
+ || store->observed == UINT64_MAX)
478
+ return tf_transform_set_error(
479
+ error, TF_TRANSFORM_RESOURCE_LIMIT,
480
+ "categorical inference count overflows");
481
+ ++store->slots[slot_index].count;
482
+ ++store->observed;
483
+ return TF_TRANSFORM_OK;
484
+ }
485
+ if ((uint64_t)store->category_count >= max_categories) {
486
+ code = category_inference_resolve_numeric(analyzer, store, error);
487
+ if (code == TF_TRANSFORM_OK) *resolved_numeric = 1;
488
+ return code;
489
+ }
490
+ return tf_transform_category_observe(
491
+ analyzer, column_index, value, dtype, error);
492
+ }
493
+
494
+ tf_transform_code tf_transform_analyzer_resolve_kind(
495
+ const tf_transform_analyzer *analyzer, size_t column_index,
496
+ tf_transform_column_kind *out, tf_transform_error **error) {
497
+ const tf_transform_recipe_column *recipe;
498
+ const tf_transform_category_store *store;
499
+ if (!analyzer || !out
500
+ || column_index >= analyzer->input_schema.field_count)
501
+ return tf_transform_set_error(
502
+ error, TF_TRANSFORM_INTERNAL,
503
+ "column-kind resolution arguments are invalid");
504
+ recipe = &analyzer->recipe->columns[column_index];
505
+ if (recipe->kind == TF_TRANSFORM_KIND_NUMERIC
506
+ || recipe->kind == TF_TRANSFORM_KIND_CATEGORICAL) {
507
+ *out = recipe->kind;
508
+ return TF_TRANSFORM_OK;
509
+ }
510
+ if (recipe->kind != TF_TRANSFORM_KIND_INFER
511
+ || !analyzer->category_stores)
512
+ return tf_transform_set_error(
513
+ error, TF_TRANSFORM_INTERNAL,
514
+ "column-kind inference state is unavailable");
515
+ if (analyzer->total_rows == 0)
516
+ return tf_transform_set_error(
517
+ error, TF_TRANSFORM_INSUFFICIENT_DATA,
518
+ "kind inference requires at least one analyzed row");
519
+ store = &analyzer->category_stores[column_index];
520
+ if (store->inference_numeric || store->category_count < 2) {
521
+ *out = TF_TRANSFORM_KIND_NUMERIC;
522
+ return TF_TRANSFORM_OK;
523
+ }
524
+ if ((uint64_t)store->category_count > recipe->infer_max_categories)
525
+ return tf_transform_set_error(
526
+ error, TF_TRANSFORM_INTERNAL,
527
+ "categorical inference cardinality is inconsistent");
528
+ *out = TF_TRANSFORM_KIND_CATEGORICAL;
529
+ return TF_TRANSFORM_OK;
530
+ }
531
+
532
+ tf_transform_code tf_transform_category_check_observed(
533
+ const tf_transform_analyzer *analyzer, size_t column_index,
534
+ uint64_t observed, tf_transform_error **error) {
535
+ if (!analyzer || !analyzer->category_stores
536
+ || column_index >= analyzer->input_schema.field_count)
537
+ return tf_transform_set_error(
538
+ error, TF_TRANSFORM_INTERNAL,
539
+ "categorical analyzer state is unavailable");
540
+ if (analyzer->recipe->columns[column_index].kind
541
+ == TF_TRANSFORM_KIND_CATEGORICAL
542
+ && analyzer->recipe->columns[column_index].categorical_impute
543
+ == TF_TRANSFORM_CATEGORICAL_IMPUTE_NONE
544
+ && analyzer->recipe->columns[column_index].categorical_encode
545
+ == TF_TRANSFORM_ENCODE_NONE)
546
+ return TF_TRANSFORM_OK;
547
+ if (analyzer->recipe->columns[column_index].kind == TF_TRANSFORM_KIND_INFER
548
+ && analyzer->category_stores[column_index].inference_numeric)
549
+ return TF_TRANSFORM_OK;
550
+ if (analyzer->category_stores[column_index].observed != observed)
551
+ return tf_transform_set_error(
552
+ error, TF_TRANSFORM_INTERNAL,
553
+ "categorical retained state does not match analyzer statistics");
554
+ return TF_TRANSFORM_OK;
555
+ }
556
+
557
+ tf_transform_code tf_transform_category_plan_requirements(
558
+ const tf_transform_analyzer *analyzer, size_t column_index,
559
+ uint64_t *resident_bytes, uint64_t *allocation_count,
560
+ tf_transform_error **error) {
561
+ const tf_transform_category_store *store;
562
+ const tf_transform_recipe_column *recipe;
563
+ uint64_t count;
564
+ if (!analyzer || !analyzer->category_stores
565
+ || column_index >= analyzer->input_schema.field_count
566
+ || !resident_bytes || !allocation_count)
567
+ return tf_transform_set_error(
568
+ error, TF_TRANSFORM_INTERNAL,
569
+ "categorical plan requirement arguments are invalid");
570
+ store = &analyzer->category_stores[column_index];
571
+ recipe = &analyzer->recipe->columns[column_index];
572
+ if (recipe->kind == TF_TRANSFORM_KIND_CATEGORICAL
573
+ && recipe->categorical_impute == TF_TRANSFORM_CATEGORICAL_IMPUTE_NONE
574
+ && recipe->categorical_encode == TF_TRANSFORM_ENCODE_NONE) {
575
+ *resident_bytes = 0;
576
+ *allocation_count = 0;
577
+ return TF_TRANSFORM_OK;
578
+ }
579
+ if (analyzer->total_rows == 0
580
+ && !(recipe->categorical_fixed_count
581
+ && recipe->categorical_impute == TF_TRANSFORM_CATEGORICAL_IMPUTE_NONE))
582
+ return tf_transform_set_error(
583
+ error, TF_TRANSFORM_INSUFFICIENT_DATA,
584
+ "categorical mode requires at least one analyzed row");
585
+ count = recipe->categorical_fixed_count
586
+ ? (uint64_t)recipe->categorical_fixed_count : (uint64_t)store->category_count;
587
+ if (recipe->kind == TF_TRANSFORM_KIND_INFER
588
+ && (count < 2 || count > recipe->infer_max_categories))
589
+ return tf_transform_set_error(
590
+ error, TF_TRANSFORM_INTERNAL,
591
+ "inferred categorical cardinality is inconsistent");
592
+ if (count == 0) {
593
+ if (recipe->categorical_impute != TF_TRANSFORM_CATEGORICAL_IMPUTE_MODE
594
+ || recipe->categorical_all_missing != TF_TRANSFORM_ALL_MISSING_ZERO)
595
+ return tf_transform_set_error(
596
+ error, TF_TRANSFORM_INSUFFICIENT_DATA,
597
+ "categorical mode has no observed values");
598
+ count = 1;
599
+ }
600
+ if (recipe->categorical_encode == TF_TRANSFORM_ENCODE_LABEL
601
+ || recipe->categorical_encode == TF_TRANSFORM_ENCODE_ONEHOT) {
602
+ if (count > (uint64_t)TF_TRANSFORM_MAX_SAFE_INTEGER_V1)
603
+ return tf_transform_set_error(
604
+ error, TF_TRANSFORM_RESOURCE_LIMIT,
605
+ "categorical encoding count exceeds the safe-integer domain");
606
+ if (recipe->categorical_encode == TF_TRANSFORM_ENCODE_LABEL
607
+ && recipe->categorical_unknown == TF_TRANSFORM_UNKNOWN_SENTINEL
608
+ && recipe->categorical_sentinel_label >= 0
609
+ && (uint64_t)recipe->categorical_sentinel_label < count)
610
+ return tf_transform_set_error(
611
+ error, TF_TRANSFORM_INVALID_RECIPE,
612
+ "categorical sentinel collides with a learned label");
613
+ }
614
+ if (count > analyzer->runtime.limits.max_categories_per_column
615
+ || count > analyzer->runtime.limits.max_total_categories
616
+ || count > SIZE_MAX / sizeof(tf_transform_category_value)
617
+ || count * sizeof(tf_transform_category_value)
618
+ > analyzer->runtime.limits.max_allocation_bytes)
619
+ return tf_transform_set_error(
620
+ error, TF_TRANSFORM_RESOURCE_LIMIT,
621
+ "categorical plan state exceeds limits");
622
+ *resident_bytes = count * sizeof(tf_transform_category_value);
623
+ *allocation_count = 1;
624
+ return TF_TRANSFORM_OK;
625
+ }
626
+
627
+ static tf_transform_code category_sort_tick(
628
+ const tf_transform_runtime_copy *runtime, size_t *ticks,
629
+ tf_transform_error **error) {
630
+ ++*ticks;
631
+ if (*ticks < TF_TRANSFORM_CANCEL_ITERS_V1) return TF_TRANSFORM_OK;
632
+ *ticks = 0;
633
+ return category_poll(runtime, error);
634
+ }
635
+
636
+ static tf_transform_code category_sift_down(
637
+ tf_transform_category_value *values, size_t root, size_t end,
638
+ uint32_t dtype, const tf_transform_runtime_copy *runtime,
639
+ size_t *ticks, tf_transform_error **error) {
640
+ if (end == 0) return TF_TRANSFORM_OK;
641
+ while (root <= (end - 1) / 2) {
642
+ size_t child = root * 2 + 1;
643
+ size_t candidate = root;
644
+ tf_transform_code code = category_sort_tick(runtime, ticks, error);
645
+ if (code != TF_TRANSFORM_OK) return code;
646
+ if (tf_transform_category_compare(
647
+ values[candidate].bits, values[child].bits, dtype) < 0)
648
+ candidate = child;
649
+ if (child < end) {
650
+ code = category_sort_tick(runtime, ticks, error);
651
+ if (code != TF_TRANSFORM_OK) return code;
652
+ if (tf_transform_category_compare(
653
+ values[candidate].bits, values[child + 1].bits, dtype) < 0)
654
+ candidate = child + 1;
655
+ }
656
+ if (candidate == root) return TF_TRANSFORM_OK;
657
+ {
658
+ tf_transform_category_value temporary = values[root];
659
+ values[root] = values[candidate];
660
+ values[candidate] = temporary;
661
+ }
662
+ root = candidate;
663
+ }
664
+ return TF_TRANSFORM_OK;
665
+ }
666
+
667
+ static tf_transform_code category_sort(
668
+ tf_transform_category_value *values, size_t count, uint32_t dtype,
669
+ const tf_transform_runtime_copy *runtime, tf_transform_error **error) {
670
+ size_t ticks = 0;
671
+ tf_transform_code code = category_poll(runtime, error);
672
+ if (code != TF_TRANSFORM_OK || count < 2) return code;
673
+ for (size_t start = count / 2; start > 0; --start) {
674
+ code = category_sift_down(
675
+ values, start - 1, count - 1, dtype, runtime, &ticks, error);
676
+ if (code != TF_TRANSFORM_OK) return code;
677
+ }
678
+ for (size_t end = count - 1; end > 0; --end) {
679
+ tf_transform_category_value temporary = values[0];
680
+ values[0] = values[end];
681
+ values[end] = temporary;
682
+ code = category_sift_down(
683
+ values, 0, end - 1, dtype, runtime, &ticks, error);
684
+ if (code != TF_TRANSFORM_OK) return code;
685
+ }
686
+ return category_poll(runtime, error);
687
+ }
688
+
689
+ static tf_transform_code category_finalize_encoding(
690
+ const tf_transform_recipe_column *recipe,
691
+ tf_transform_categorical_state *out, tf_transform_error **error) {
692
+ if (!recipe || !out)
693
+ return tf_transform_set_error(
694
+ error, TF_TRANSFORM_INTERNAL,
695
+ "categorical encoding state is invalid");
696
+ out->unknown = recipe->categorical_unknown;
697
+ out->sentinel_label = recipe->categorical_sentinel_label;
698
+ out->has_sentinel_label = recipe->categorical_has_sentinel_label;
699
+ if (out->encode != TF_TRANSFORM_ENCODE_LABEL
700
+ && out->encode != TF_TRANSFORM_ENCODE_ONEHOT)
701
+ return TF_TRANSFORM_OK;
702
+ if ((uint64_t)out->category_count
703
+ > (uint64_t)TF_TRANSFORM_MAX_SAFE_INTEGER_V1)
704
+ return tf_transform_set_error(
705
+ error, TF_TRANSFORM_RESOURCE_LIMIT,
706
+ "categorical encoding count exceeds the safe-integer domain");
707
+ if (out->encode == TF_TRANSFORM_ENCODE_ONEHOT) {
708
+ out->sentinel_label = 0;
709
+ out->has_sentinel_label = 0;
710
+ if (out->unknown == TF_TRANSFORM_UNKNOWN_OTHER) {
711
+ out->other_ordinal = (uint64_t)out->category_count;
712
+ out->has_other_ordinal = 1;
713
+ } else if (out->unknown != TF_TRANSFORM_UNKNOWN_ERROR
714
+ && out->unknown != TF_TRANSFORM_UNKNOWN_ALL_ZERO) {
715
+ return tf_transform_set_error(
716
+ error, TF_TRANSFORM_INVALID_RECIPE,
717
+ "one-hot unknown-category policy is invalid");
718
+ }
719
+ return TF_TRANSFORM_OK;
720
+ }
721
+ if (out->unknown == TF_TRANSFORM_UNKNOWN_SENTINEL) {
722
+ if (!out->has_sentinel_label)
723
+ return tf_transform_set_error(
724
+ error, TF_TRANSFORM_INTERNAL,
725
+ "categorical sentinel state is missing");
726
+ if (out->sentinel_label >= 0
727
+ && (uint64_t)out->sentinel_label < (uint64_t)out->category_count)
728
+ return tf_transform_set_error(
729
+ error, TF_TRANSFORM_INVALID_RECIPE,
730
+ "categorical sentinel collides with a learned label");
731
+ } else if (out->unknown == TF_TRANSFORM_UNKNOWN_OTHER) {
732
+ out->other_ordinal = (uint64_t)out->category_count;
733
+ out->has_other_ordinal = 1;
734
+ }
735
+ return TF_TRANSFORM_OK;
736
+ }
737
+
738
+ tf_transform_code tf_transform_category_finalize(
739
+ const tf_transform_analyzer *analyzer, size_t column_index,
740
+ tf_transform_categorical_state *out, tf_transform_error **error) {
741
+ const tf_transform_category_store *store;
742
+ const tf_transform_recipe_column *recipe;
743
+ uint64_t resident = 0;
744
+ uint64_t allocations = 0;
745
+ uint64_t best_bits = 0;
746
+ uint64_t best_count = 0;
747
+ size_t output_index = 0;
748
+ tf_transform_code code;
749
+ if (!out)
750
+ return tf_transform_set_error(
751
+ error, TF_TRANSFORM_INTERNAL,
752
+ "categorical plan output is null");
753
+ memset(out, 0, sizeof(*out));
754
+ code = tf_transform_category_plan_requirements(
755
+ analyzer, column_index, &resident, &allocations, error);
756
+ if (code != TF_TRANSFORM_OK) return code;
757
+ (void)allocations;
758
+ store = &analyzer->category_stores[column_index];
759
+ recipe = &analyzer->recipe->columns[column_index];
760
+ out->impute = recipe->categorical_impute;
761
+ out->all_missing = recipe->categorical_all_missing;
762
+ out->encode = recipe->categorical_encode;
763
+ out->source_dtype = analyzer->input_schema.fields[column_index].dtype;
764
+ out->category_count = (size_t)(resident / sizeof(*out->categories));
765
+ if (resident % sizeof(*out->categories) != 0)
766
+ return tf_transform_set_error(
767
+ error, TF_TRANSFORM_INTERNAL,
768
+ "categorical plan requirement count is invalid");
769
+ if (out->category_count == 0) {
770
+ if (recipe->kind != TF_TRANSFORM_KIND_CATEGORICAL
771
+ || recipe->categorical_impute != TF_TRANSFORM_CATEGORICAL_IMPUTE_NONE
772
+ || recipe->categorical_encode != TF_TRANSFORM_ENCODE_NONE)
773
+ return tf_transform_set_error(
774
+ error, TF_TRANSFORM_INTERNAL,
775
+ "categorical plan has an illegal empty dictionary");
776
+ code = category_finalize_encoding(recipe, out, error);
777
+ if (code != TF_TRANSFORM_OK) return code;
778
+ return category_poll(&analyzer->runtime, error);
779
+ }
780
+ code = category_poll(&analyzer->runtime, error);
781
+ if (code != TF_TRANSFORM_OK) return code;
782
+ out->categories = (tf_transform_category_value *)calloc(
783
+ out->category_count, sizeof(*out->categories));
784
+ if (!out->categories)
785
+ return tf_transform_set_error(
786
+ error, TF_TRANSFORM_ALLOCATION,
787
+ "categorical plan allocation failed");
788
+ if (recipe->categorical_fixed_count) {
789
+ code = tf_transform_copy_bytes_runtime(
790
+ out->categories, recipe->categorical_fixed, (size_t)resident,
791
+ &analyzer->runtime, error);
792
+ if (code != TF_TRANSFORM_OK) goto failed;
793
+ for (size_t i = 0; i < store->capacity; ++i) {
794
+ if (i % TF_TRANSFORM_CANCEL_ITERS_V1 == 0) {
795
+ code = category_poll(&analyzer->runtime, error);
796
+ if (code != TF_TRANSFORM_OK) goto failed;
797
+ }
798
+ const tf_transform_category_slot *slot = &store->slots[i];
799
+ if (slot->occupied && (slot->count > best_count
800
+ || (slot->count == best_count && tf_transform_category_compare(
801
+ slot->bits, best_bits, store->dtype) < 0))) {
802
+ best_count = slot->count;
803
+ best_bits = slot->bits;
804
+ }
805
+ }
806
+ if (recipe->categorical_impute == TF_TRANSFORM_CATEGORICAL_IMPUTE_MODE) {
807
+ if (!best_count) {
808
+ int zero_known = 0;
809
+ code = fixed_category_known(recipe, 0, &analyzer->runtime, &zero_known, error);
810
+ if (code != TF_TRANSFORM_OK) goto failed;
811
+ if (recipe->categorical_all_missing != TF_TRANSFORM_ALL_MISSING_ZERO || !zero_known) {
812
+ code = tf_transform_set_error(error, TF_TRANSFORM_INSUFFICIENT_DATA,
813
+ "fixed dictionary mode has no known observations or declared zero fallback");
814
+ goto failed;
815
+ }
816
+ best_bits = 0;
817
+ }
818
+ out->impute_bits = best_bits;
819
+ out->has_impute_value = 1;
820
+ }
821
+ code = category_finalize_encoding(recipe, out, error);
822
+ if (code != TF_TRANSFORM_OK) goto failed;
823
+ return category_poll(&analyzer->runtime, error);
824
+ }
825
+ if (store->category_count == 0) {
826
+ out->categories[0].bits = 0;
827
+ out->impute_bits = 0;
828
+ out->has_impute_value = 1;
829
+ code = category_finalize_encoding(recipe, out, error);
830
+ if (code != TF_TRANSFORM_OK) goto failed;
831
+ return category_poll(&analyzer->runtime, error);
832
+ }
833
+ for (size_t i = 0; i < store->capacity; ++i) {
834
+ const tf_transform_category_slot *slot;
835
+ if (i % TF_TRANSFORM_CANCEL_ITERS_V1 == 0) {
836
+ code = category_poll(&analyzer->runtime, error);
837
+ if (code != TF_TRANSFORM_OK) goto failed;
838
+ }
839
+ slot = &store->slots[i];
840
+ if (!slot->occupied) continue;
841
+ if (output_index >= out->category_count) {
842
+ code = tf_transform_set_error(
843
+ error, TF_TRANSFORM_INTERNAL,
844
+ "categorical plan category count drifted");
845
+ goto failed;
846
+ }
847
+ out->categories[output_index++].bits = slot->bits;
848
+ if (slot->count > best_count
849
+ || (slot->count == best_count
850
+ && tf_transform_category_compare(
851
+ slot->bits, best_bits, store->dtype) < 0)) {
852
+ best_count = slot->count;
853
+ best_bits = slot->bits;
854
+ }
855
+ }
856
+ if (output_index != out->category_count
857
+ || (recipe->categorical_impute == TF_TRANSFORM_CATEGORICAL_IMPUTE_MODE
858
+ && best_count == 0)) {
859
+ code = tf_transform_set_error(
860
+ error, TF_TRANSFORM_INTERNAL,
861
+ "categorical plan state is incomplete");
862
+ goto failed;
863
+ }
864
+ code = category_sort(
865
+ out->categories, out->category_count, out->source_dtype,
866
+ &analyzer->runtime, error);
867
+ if (code != TF_TRANSFORM_OK) goto failed;
868
+ if (recipe->categorical_impute == TF_TRANSFORM_CATEGORICAL_IMPUTE_MODE) {
869
+ out->impute_bits = best_bits;
870
+ out->has_impute_value = 1;
871
+ }
872
+ code = category_finalize_encoding(recipe, out, error);
873
+ if (code != TF_TRANSFORM_OK) goto failed;
874
+ return TF_TRANSFORM_OK;
875
+ failed:
876
+ tf_transform_categorical_state_clear(out);
877
+ return code;
878
+ }
879
+
880
+ void tf_transform_categorical_state_clear(tf_transform_categorical_state *state) {
881
+ if (!state) return;
882
+ free(state->categories);
883
+ memset(state, 0, sizeof(*state));
884
+ }
885
+
886
+ tf_transform_code tf_transform_category_lookup(
887
+ const tf_transform_categorical_state *state, uint64_t bits,
888
+ const tf_transform_runtime_copy *runtime, size_t *ordinal, int *found,
889
+ tf_transform_error **error) {
890
+ size_t lower = 0;
891
+ size_t upper;
892
+ size_t ticks = 0;
893
+ tf_transform_code code;
894
+ if (!state || !runtime || !ordinal || !found)
895
+ return tf_transform_set_error(
896
+ error, TF_TRANSFORM_INTERNAL,
897
+ "categorical lookup arguments are invalid");
898
+ upper = state->category_count;
899
+ code = tf_transform_poll_cancel(runtime, error);
900
+ if (code != TF_TRANSFORM_OK) return code;
901
+ while (lower < upper) {
902
+ size_t middle = lower + (upper - lower) / 2;
903
+ int comparison;
904
+ ++ticks;
905
+ if (ticks >= TF_TRANSFORM_CANCEL_ITERS_V1) {
906
+ ticks = 0;
907
+ code = tf_transform_poll_cancel(runtime, error);
908
+ } else code = TF_TRANSFORM_OK;
909
+ if (code != TF_TRANSFORM_OK) return code;
910
+ comparison = tf_transform_category_compare(
911
+ state->categories[middle].bits, bits, state->source_dtype);
912
+ if (comparison < 0) lower = middle + 1;
913
+ else if (comparison > 0) upper = middle;
914
+ else {
915
+ *ordinal = middle;
916
+ *found = 1;
917
+ return TF_TRANSFORM_OK;
918
+ }
919
+ }
920
+ *ordinal = 0;
921
+ *found = 0;
922
+ return TF_TRANSFORM_OK;
923
+ }